<file_sep>import Foundation
import SwiftyJSON

public struct Manifest {
    
    public struct File: Equatable {
        
        public let path: String
        public let hash: String
        
        public init(path: String, hash: String) {
            self.path = path
            self.hash = hash
        }
    }
    
    // MARK: - Attributes
    
    public let baseUrl: String
    public let commit: String?
    public let buildAuthor: String?
    public let gitBranch: String?
    public let timestamp: Int?
    public let files: [Manifest.File]
    
    
    // MARK: - Init
    
    public init(baseUrl: String,
                commit: String?,
                buildAuthor: String?,
                gitBranch: String?,
                timestamp: Int?,
                files: [Manifest.File]) {
        self.baseUrl = baseUrl
        self.commit = commit
        self.buildAuthor = buildAuthor
        self.gitBranch = gitBranch
        self.timestamp = timestamp
        self.files = files
    }
    
}

public func == (lhs: Manifest.File, rhs: Manifest.File) -> Bool {
    return lhs.hash == rhs.hash && lhs.path == rhs.path
}

// MARK: - Manifest Extension (Data)

internal extension Manifest {
    
    internal static var LocalName: String = "app.json"
    
    internal init(data: Data) throws {
        let json = JSON(data: data)
        guard let baseUrl = json["base_url"].string else { throw FrontendManifestError.invalidManifest("Missing key: base_url") }
        let commit = json["commit"].string
        let buildAuthor = json["build_author"].string
        let gitBranch = json["git_branch"].string
        let timestamp = json["timestamp"].int
        let files = json["files"].arrayValue.flatMap { (json) -> Manifest.File? in
            guard let path = json["path"].string, let hash = json["hash"].string else { return nil }
            return Manifest.File(path: path, hash: hash)
        }
        self.init(baseUrl: baseUrl,
                        commit: commit,
                        buildAuthor: buildAuthor,
                        gitBranch: gitBranch,
                        timestamp: timestamp,
                        files: files)
    }
    
    internal func toData() throws -> Data {
        var dictionary = [String: AnyObject]()
        dictionary["base_url"] = self.baseUrl as AnyObject?
        if let commit = self.commit { dictionary["commit"] = commit as AnyObject? }
        if let buildAuthor = self.buildAuthor { dictionary["build_author"] = buildAuthor as AnyObject? }
        if let gitBranch = self.gitBranch { dictionary["git_branch"] = gitBranch as AnyObject? }
        if let timestamp = self.timestamp { dictionary["timestamp"] = timestamp as AnyObject? }
        var files: [[String: AnyObject]] = []
        self.files.forEach { file in
         files.append(["path": file.path as AnyObject, "hash": file.hash as AnyObject])
        }
        dictionary["files"] = files as AnyObject?
        do {
            return try JSONSerialization.data(withJSONObject: dictionary, options: [])
        }
        catch {
            throw FrontendManifestError.unconvertibleJSON
        }
    }
    
}
<file_sep>require "spec_helper"
require 'webmock/rspec'
require 'zip'
require 'json'

describe "Command" do

  before :each do
    @tmp_folder = File.join File.expand_path(Dir.pwd), "tmp"
    @manifest_url = "https://test.com/manifest.json"
    @download_path = File.join File.expand_path(Dir.pwd), "frontend.zip"
    @subject = Frontend::Command.new(@manifest_url, @download_path)
    @manifest_json = {files:
      {
          "a/file.txt": "2222",
          "b/file.txt": "44444"
      }
    }
    stub_request(:get, @manifest_url).to_return(body: @manifest_json.to_json)
    stub_request(:get, "https://test.com/a/file.txt").to_return(body: "test1")
    stub_request(:get, "https://test.com/b/file.txt").to_return(body: "test2")
  end

  describe "frontend download" do
    before :each do
      @subject.execute
      FileUtils.rm_rf @tmp_folder
      FileUtils.mkdir @tmp_folder
      Zip::File.open(@download_path) do |zip_file|
        zip_file.each do |entry|
          f_path=File.join(@tmp_folder, entry.name)
          FileUtils.mkdir_p(File.dirname(f_path))
          entry.extract(f_path) unless File.exist?(f_path)
        end
      end
    end

    after :each do
      FileUtils.rm_rf @download_path
      FileUtils.rm_rf @tmp_folder
    end

    it "should zip the file" do
      expect(File.exist?(@download_path)).to be_truthy
    end

    describe "manifest" do
      it "should include the base_url" do
        manifest_path = File.join @tmp_folder, "app.json"
        manifest = JSON.parse(File.read(manifest_path))
        expect(manifest["base_url"]).to eq("https://test.com")
      end

      it "should contain all the files" do
        manifest_path = File.join @tmp_folder, "app.json"
        manifest = JSON.parse(File.read(manifest_path))
        expect(manifest["files"].count).to eq(2)
      end
    end

    describe "files" do
      it "should contain the files in the correct folders" do
        a_file = File.read(File.join(@tmp_folder, "a/file.txt"))
        b_file = File.read(File.join @tmp_folder, "b/file.txt")
        expect(a_file).to eq("test1\n")
        expect(b_file).to eq("test2\n")
      end
    end
  end

end
<file_sep>import Foundation
import GCDWebServer

internal class FrontendProxyRequestMapper {
    
    // MARK: - Internal
    
    internal func map(method method: String!, url: URL!, headers: [AnyHashable: Any]!, path: String!, query: [AnyHashable: Any]!, proxyResources: [ProxyResource]) -> GCDWebServerRequest? {
        return proxyResources.flatMap { [weak self] (resource) -> GCDWebServerRequest? in
            return self?.request(withResource: resource, method: method, url: url, headers: headers, path: path, query: query)
        }.first
    }
    
    // MARK: - Private
    
    private func request(withResource resource: ProxyResource, method: String!, url: URL!, headers: [AnyHashable: Any]!, path: String!, query: [AnyHashable: Any]!) -> GCDWebServerRequest? {
        guard let url = url else { return nil }
        guard let path = path else { return nil }
        if !path.contains(resource.path) { return nil }
        let proxiedUrl = URL(string: resource.url)!
        return GCDWebServerRequest(method: method,
                                   url: proxiedUrl,
                                   headers: headers,
                                   path: path,
                                   query: query)
        
    }
    
}
<file_sep>import Foundation
import Quick
import Nimble
import Zip

@testable import Frontend

private class TestClass: NSObject {}

private func unzip(name: String, path: String) {
    let url = Bundle(for: TestClass.self).url(forResource: name, withExtension: "zip")
    try! Zip.unzipFile(url!, destination: URL(fileURLWithPath: path), overwrite: true, password: nil, progress: nil)
}

class FrontendFileManagerSpec: QuickSpec {
    override func spec() {
        
        var path: String!
        var zipPath: String!
        var fileManager: MockFileManager!
        var zipExtractor: MockZipExtractor!
        var subject: FrontendFileManager!
        
        beforeEach {
            path = FrontendConfiguration.defaultDirectory()
            zipPath = "zipPath"
            fileManager = MockFileManager()
            zipExtractor = MockZipExtractor()
            unzip(name: "Data", path: path)
            subject = FrontendFileManager(path: path, zipPath: zipPath, fileManager: fileManager, zipExtractor: zipExtractor)
        }
        
        describe("-currentPath") {
            it("should return the correct value") {
                expect(subject.currentPath()) == "\(path!)/Current"
            }
        }
        
        describe("-enqueuedPath") {
            it("should return the correct value") {
                expect(subject.enqueuedPath()) == "\(path!)/Enqueued"
            }
        }
        
        describe("-currentFrontendManifest") {
            it("should return the correct value") {
                expect(subject.currentFrontendManifest()).toNot(beNil())
            }
        }
        
        describe("-enqueuedFrontendManifest") {
            it("should return the correct value") {
                expect(subject.enqueuedFrontendManifest()).toNot(beNil())
            }
        }
        
        describe("-currentAvailable") {
            it("should return the correct value") {
                expect(subject.currentAvailable()) == false
            }
        }
        
        describe("-enqueuedAvailable") {
            it("should return the correct value") {
                expect(subject.enqueuedAvailable()) == true
            }
        }
        
        describe("-replaceWithEnqueued") {
            beforeEach {
                try! subject.replaceWithEnqueued()
            }
            it("should remove the current one") {
                expect(fileManager.removedAtPath) == subject.currentPath()
            }
            it("should copy the enqueued") {
                expect(fileManager.movedFrom) == subject.enqueuedPath()
                expect(fileManager.movedTo) == subject.currentPath()
            }
        }
        
        describe("-replaceWithZipped") {
            beforeEach {
                try! subject.replaceWithZipped()
            }
            it("should take the correct zip") {
                expect(zipExtractor.zipUrl) == URL(fileURLWithPath: subject.zipPath)
            }
            it("should extract into the correct directory") {
                expect(zipExtractor.destinationUrl) == URL(fileURLWithPath: subject.currentPath())
            }
            it("should extract it overwriting the existing content") {
                expect(zipExtractor.overwrite) == true
            }
        }
    }
}

// MARK: - Mocks

private class MockZipExtractor: ZipExtractor {
    
    var zipUrl: URL!
    var destinationUrl: URL!
    var overwrite: Bool!
    
    fileprivate override func unzipFile(zipFilePath: URL, destination: URL, overwrite: Bool, password: String?, progress: ((_ progress: Double) -> ())?) throws {
        self.zipUrl = zipFilePath
        self.destinationUrl = destination
        self.overwrite = overwrite
    }
}

private class MockFileManager: FileManager {
    var removedAtPath: String!
    var movedFrom: String!
    var movedTo: String!
    fileprivate override func removeItem(atPath path: String) throws {
        self.removedAtPath = path
    }
    
    fileprivate override func moveItem(atPath srcPath: String, toPath dstPath: String) throws {
        self.movedFrom = srcPath
        self.movedTo = dstPath
    }
}
<file_sep>import Foundation
import Zip

internal class ZipExtractor {
    
    internal func unzipFile(zipFilePath: URL, destination: URL, overwrite: Bool, password: String?, progress: ((_ progress: Double) -> ())?) throws {
        try Zip.unzipFile(zipFilePath, destination: destination, overwrite: overwrite, password: <PASSWORD>, progress: progress)
    }
    
}
<file_sep>import Foundation

public struct FrontendConfiguration {
    
    // MARK: - Attributes
    
    internal let manifestUrl: String
    internal let baseUrl: String
    internal let localPath: String
    internal let port: UInt
    internal let proxyResources: [ProxyResource]
    internal let manifestMapper: ManifestMapper
    internal let zipPath: String
    
    // MARK: - Init
    
    public init(manifestUrl: String,
                baseUrl: String,
                port: UInt,
                zipPath: String,
                proxyResources: [ProxyResource] = [],
                localPath: String = FrontendConfiguration.defaultDirectory(),
                manifestMapper: @escaping ManifestMapper = DefaultManifestMapper) {
        self.manifestUrl = manifestUrl
        self.baseUrl = baseUrl
        self.localPath = localPath
        self.port = port
        self.proxyResources = proxyResources
        self.manifestMapper = manifestMapper
        self.zipPath = zipPath
    }
    
    // MARK: - Private
    
    internal static func defaultDirectory() -> String {
        let documentsDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
        return NSString(string: documentsDirectory).appendingPathComponent("Frontend")
    }
    
}
<file_sep>use_frameworks!
target 'Frontend_Tests' do
  pod 'Frontend', :path => '../'
  
  pod 'Quick', "~> 0.10"
  pod 'Nimble', '~> 5.0'
end
<file_sep>import Foundation
import Zip

internal class FrontendFileManager {
    
    // MARK: - Attributes
    
    internal let path: String
    internal let zipPath: String
    internal let fileManager: FileManager
    internal let zipExtractor: ZipExtractor
    
    // MARK: - Init
    
    internal init(path: String, zipPath: String, fileManager: FileManager = FileManager.default, zipExtractor: ZipExtractor = ZipExtractor()) {
        self.path = path
        self.zipPath = zipPath
        self.fileManager = fileManager
        self.zipExtractor = zipExtractor
    }
    
    // MARK: - Internal
    
    internal func currentPath() -> String {
        return NSString(string: self.path).appendingPathComponent("Current")
    }
    
    internal func enqueuedPath() -> String {
        return NSString(string: self.path).appendingPathComponent("Enqueued")
    }
    
    internal func currentFrontendManifest() -> Manifest? {
        return self.manifest(atPath: self.currentPath())
    }
    
    internal func enqueuedFrontendManifest() -> Manifest? {
        return self.manifest(atPath: self.enqueuedPath())
    }
    
    internal func currentAvailable() -> Bool {
        return self.frontend(atPath: self.currentPath())
    }
    
    internal func enqueuedAvailable() -> Bool {
        return self.frontend(atPath: self.enqueuedPath())
    }
    
    internal func replaceWithEnqueued() throws {
        if !self.enqueuedAvailable() { throw FrontendFileManagerError.notAvailable }
        try self.fileManager.removeItem(atPath: self.currentPath())
        try self.fileManager.moveItem(atPath: self.enqueuedPath(), toPath: self.currentPath())
    }
    
    internal func replaceWithZipped() throws {
        let zipFilePath: URL = URL(fileURLWithPath: self.zipPath)
        let destinationPath: URL = URL(fileURLWithPath: self.currentPath())
        try self.zipExtractor.unzipFile(zipFilePath: zipFilePath, destination: destinationPath, overwrite: true, password: nil, progress: nil)
    }
    
    // MARK: - Private
    
    fileprivate func manifest(atPath path: String) -> Manifest? {
        let manifestPath = NSString(string: path).appendingPathComponent(Manifest.LocalName)
        guard let data = try? Data(contentsOf: URL(fileURLWithPath: manifestPath)) else { return nil }
        return try? Manifest(data: data)
    }
    
    fileprivate func frontend(atPath path: String) -> Bool {
        guard let manifest = self.manifest(atPath: path) else { return false }
        for file in manifest.files {
            let filePath = NSString(string: path).appendingPathComponent(file.path)
            if !self.fileManager.fileExists(atPath: filePath) {
                return false
            }
        }
        return true
    }
    
}
<file_sep>import Foundation
import Quick
import Nimble
import GCDWebServer

@testable import Frontend

class FrontendRequestDispatcherSpec: QuickSpec {
    override func spec() {

        var session: MockSession!
        var dataTask: MockSessionDataTask!
        var response: GCDWebServerResponse!
        var responseMapper: MockResponseMapper!
        var request: URLRequest!
        var requestMapper: MockRequestMapper!
        var subject: FrontendRequestDispatcher!
        
        beforeEach {
            dataTask = MockSessionDataTask()
            session = MockSession()
            session.dataTask = dataTask
            response = GCDWebServerResponse()
            responseMapper = MockResponseMapper()
            responseMapper.response = response
            request = URLRequest(url: URL(string: "test://test")!)
            requestMapper = MockRequestMapper()
            requestMapper.request = request
            subject = FrontendRequestDispatcher(requestMapper: requestMapper,
                responseMapper: responseMapper,
                session: session)
        }
        
        describe("-dispatch:request:completion") {
            var dispatcherResponse: GCDWebServerResponse!
            
            beforeEach {
                subject.dispatch(request: GCDWebServerRequest(), completion: { (response) in
                    dispatcherResponse = response
                })
            }
            
            it("should complete with the correct response") {
                expect(dispatcherResponse).to(beIdenticalTo(response))
            }
        }
    }
}


// MARK: - Mocks

private class MockRequestMapper: FrontendRequestMapper {
    
    var request: URLRequest!
    
    fileprivate override func map(request: GCDWebServerRequest) -> URLRequest {
        return self.request
    }
}

private class MockResponseMapper: FrontendResponseMapper {
    
    var response: GCDWebServerResponse!
    
    fileprivate override func map(data: Data?, response: URLResponse?, error: Error?) -> GCDWebServerResponse {
        return self.response
    }
}

private class MockSession: URLSession {
    
    var dataTask: MockSessionDataTask!
    
    fileprivate override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
        completionHandler(nil, URLResponse(), nil)
        return dataTask

    }
    
}

private class MockSessionDataTask: URLSessionDataTask {
    
    var resumed: Bool = false
    
    fileprivate override func resume() {
        self.resumed = true
    }
}
<file_sep>import Foundation
import Quick
import Nimble
import GCDWebServer

@testable import Frontend

class FrontendServerSpec: QuickSpec {
    override func spec() {
        var subject: FrontendServer!
        var server: MockGCDServer!
        var path: String!
        var proxyResources: [ProxyResource]!
        var port: UInt!
        var configurator: MockFrontendConfigurator!
        
        beforeEach {
            server = MockGCDServer()
            path = "/path"
            proxyResources = []
            port = 8080
            configurator = MockFrontendConfigurator(proxyResources: [])
            subject = FrontendServer(path: path, port: port, proxyResources: proxyResources, server: server, configurator: configurator)
        }
        
        describe("-start") {
            var started: Bool!
            
            beforeEach {
                started = subject.start()
            }
            
            it("should return the correct value") {
                expect(started) == true
            }
            
            it("should start with the correct port") {
                expect(server.startedPort) == port
            }
            
            it("should start with no bonjour name") {
                expect(server.startedBonjourName).to(beNil())
            }
        
            it("should configure the frontend with the correct directory path") {
                expect(configurator.configuredDirectoryPath) == path
            }
            
            it("should configure the frontend with the correct server") {
                expect(configurator.configuredServer).to(beIdenticalTo(subject.server))
            }
        }
    }
}

// MARK: - Mock

private class MockGCDServer: GCDWebServer {
    
    var startedPort: UInt!
    var startedBonjourName: String!
    
    fileprivate override func start(withPort port: UInt, bonjourName name: String!) -> Bool {
        self.startedPort = port
        self.startedBonjourName = name
        return true
    }

}

private class MockFrontendConfigurator: FrontendConfigurator {
    
    var configuredDirectoryPath: String!
    var configuredServer: GCDWebServer!
    
    fileprivate override func configure(directoryPath: String, server: GCDWebServer) {
        self.configuredDirectoryPath = directoryPath
        self.configuredServer = server
    }
}
<file_sep>import Foundation
import Quick
import Nimble

@testable import Frontend

class FrontendControllerSpec: QuickSpec {
    override func spec() {
        
        var configuration: FrontendConfiguration!
        var fileManager: MockFileManager!
        var downloader: MockDownloader!
        var server: MockServer!
        var subject: FrontendController!
        
        beforeEach {
            configuration = FrontendConfiguration(manifestUrl: "manifestUrl", baseUrl: "baseURl", port: 8080, zipPath: "zipPath")
            fileManager = MockFileManager(path: configuration.localPath, zipPath: configuration.zipPath)
            downloader = MockDownloader()
            server = MockServer(path: fileManager.currentPath(), port: configuration.port, proxyResources: configuration.proxyResources)
            subject = FrontendController(configuration: configuration,
                fileManager: fileManager,
                downloader: downloader,
                server: server)
        }
        
        describe("-setup") {
            context("when the current frontend is not available") {
                beforeEach {
                    fileManager._currentAvailable = false
                    _ = try? subject.setup()
                }
                it("should replace the current with the zipped one") {
                    expect(fileManager.replacedWithZip) == true
                }
            }
            context("when the there's a current frontend and an enqueued available") {
                beforeEach {
                    fileManager._currentAvailable = true
                    fileManager._enqueuedAvailable = true
                    try! subject.setup()
                }
                it("should replace the current with the enqueued one") {
                    expect(fileManager.replacedWithEnqueued) == true
                }
            }
            context("if there's no frontend available") {
                beforeEach {
                    fileManager._currentAvailable = false
                }
                it("should throw a .NoFrontendAvailable") {
                    expect {
                        try subject.setup()
                    }.to(throwError(FrontendControllerError.noFrontendAvailable))
                }
            }
            describe("download") {
                beforeEach {
                    fileManager._currentAvailable = true
                    try! subject.setup()
                }
                it("it should download with the correct manifest") {
                    expect(downloader.downloadManifestUrl) == configuration.manifestUrl
                }
                it("should download with the correct base url") {
                    expect(downloader.downloadBaseUrl) == configuration.baseUrl
                }
                it("should download with the correct download path") {
                    expect(downloader.downloadPath) == fileManager.enqueuedPath()
                }
            }
            
            describe("server") {
                beforeEach {
                    fileManager._currentAvailable = true
                    try! subject.setup()
                }
                it("should start the server") {
                    expect(server.started) == true
                }
            }
        }
        
        describe("-url") {
            it("should have the correct format") {
                expect(subject.url()) == "http://127.0.0.1:\(configuration.port)"
            }
        }
        
        describe("-available") {
            it("should return the correct status") {
                expect(subject.available()) == fileManager.currentAvailable()
            }
        }
        
        describe("-download:replacing:progress:completion") {
            context("when it errors") {
                it("should notify about the error") {
                    waitUntil(action: { (done) in
                        let error = NSError(domain: "", code: -1, userInfo: nil)
                        downloader.completionError = error
                        try! subject.download(replacing: false, progress: nil, completion: { (_error) in
                            done()
                        })
                    })
                }
            }
            context("when it doesn't error") {
                it("shouldn't send any error back") {
                    waitUntil(action: { (done) in
                        try! subject.download(replacing: true, progress: nil, completion: { (_error) in
                            expect(_error).to(beNil())
                            done()
                        })
                    })
                }
                it("should replace if the frontend if it's specified") {
                    waitUntil(action: { (done) in
                        try! subject.download(replacing: true, progress: nil, completion: { (_error) in
                            expect(fileManager.replacedWithEnqueued) == true
                            done()
                        })
                    })
                }
            }
        }
    }
}

// MARK: - Mocks

private class MockFileManager: FrontendFileManager {
    
    var replacedWithZip: Bool = false
    var replacedWithEnqueued: Bool = false
    var _currentAvailable: Bool = false
    var _enqueuedAvailable: Bool = false
    
    fileprivate override func replaceWithZipped() throws {
        self.replacedWithZip = true
    }
    
    fileprivate override func replaceWithEnqueued() throws {
        self.replacedWithEnqueued = true
    }
    
    fileprivate override func currentAvailable() -> Bool {
        return self._currentAvailable
    }
    
    fileprivate override func enqueuedAvailable() -> Bool {
        return self._enqueuedAvailable
    }
}

private class MockDownloader: FrontendDownloader {
    
    var downloadManifestUrl: String!
    var downloadBaseUrl: String!
    var downloadPath: String!
    var completionError: Error?

    fileprivate override func download(manifestUrl: String, baseUrl: String, manifestMapper: @escaping ManifestMapper, downloadPath: String, progress: @escaping (_ downloaded: Int, _ total: Int) -> Void, completion: @escaping (Error?) -> Void) {
        self.downloadManifestUrl = manifestUrl
        self.downloadBaseUrl = baseUrl
        self.downloadPath = downloadPath
        completion(completionError)
    }
}

private class MockServer: FrontendServer {
    
    var started: Bool = false
    
    fileprivate override func start() -> Bool {
        self.started = true
        return true
    }
    
}
<file_sep>import Foundation
import Quick
import Nimble
import GCDWebServer

@testable import Frontend

class FrontendProxyRequestMapperSpec: QuickSpec {
    override func spec() {
        
        var subject: FrontendProxyRequestMapper!
        var resources: [ProxyResource]!
        var request: GCDWebServerRequest!
        
        beforeEach {
            subject = FrontendProxyRequestMapper()
            resources = [ProxyResource(path: "/admin", url: "https://remote.com")]
        }
        
        context("when the url is nil") {
            beforeEach {
                request = subject.map(method: "GET", url: nil, headers: nil, path: nil, query: nil, proxyResources: resources)
            }
            it("it shouldn't return any request") {
                expect(request).to(beNil())
            }
        }
        
        context("when the path is nil") {
            beforeEach {
                request = subject.map(method: "GET", url: URL(string: "https://test.com")!, headers: nil, path: nil, query: nil, proxyResources: resources)
            }
            it("it shouldn't return any request") {
                expect(request).to(beNil())
            }
        }
        
        context("when the path is not handled by any of the proxy resources") {
            beforeEach {
                request = subject.map(method: "GET", url: URL(string: "https://1172.16.31.10"), headers: nil, path: "/test", query: nil, proxyResources: resources)
            }
            it("shouldn't return any request") {
                expect(request).to(beNil())
            }
        }
        
        context("when the path is supported by one of the handlers") {
            beforeEach {
                request = subject.map(method: "GET", url: URL(string: "https://1172.16.31.10"), headers: ["key" as NSObject: "value" as AnyObject], path: "/admin/test/test2", query: ["key2" as NSObject: "value2" as AnyObject], proxyResources: resources)
            }
            
            it("should proxy the request with the correct method") {
                expect(request.method) == "GET"
            }
            
            it("should proxy the request with the correct url") {
                expect(request.url.absoluteString) == resources.first?.url
            }
            
            it("should proxy the request with the correct headers") {
                expect(request.headers as? [String: String]) == ["key": "value"]
            }
            
            it("should proxy the request with the correct query") {
                expect(request.query as? [String: String]) == ["key2": "value2"]
            }
        }
        
    }
}
<file_sep>import Foundation
import Quick
import Nimble
import GCDWebServer

@testable import Frontend

class FrontendresponseMapperSpec: QuickSpec {
    override func spec() {
        
        var response: HTTPURLResponse!
        var data: Data!
        var error: NSError!
        var subject: FrontendResponseMapper!
        var output: GCDWebServerResponse!
        
        beforeEach {
            response = HTTPURLResponse(url: URL(string:"http://test.com")!, statusCode: 25, httpVersion: "2.2", headerFields: ["a": "b", "content-type": "jpg"])
            subject = FrontendResponseMapper()
            data = try! JSONSerialization.data(withJSONObject: ["c": "d"], options: [])
            error = NSError(domain: "", code: -1, userInfo: nil)
            output = subject.map(data: data, response: response, error: error)
        }
        
        it("should use the correct data") {
            let outputData = try! output.readData()
            let json = try! JSONSerialization.jsonObject(with: outputData, options: []) as? [String: String]
            expect(json?["c"]) == "d"
        }
        
        it("should copy the content type") {
            expect(output.contentType) == "jpg"
        }
        
        it("should copy the status code") {
            expect(output.statusCode) == 25
        }
        
    }
}
<file_sep>import Foundation
import Quick
import Nimble
import GCDWebServer

@testable import Frontend

class FrontendRequestMapperSpec: QuickSpec {
    override func spec() {
       
        var sourceRequest: GCDWebServerRequest!
        var outputRequest: URLRequest!
        var subject: FrontendRequestMapper!
        
        beforeEach {
            sourceRequest = GCDWebServerRequest(method: "GET", url: URL(string: "http://test.com"), headers: ["a": "b"], path: "/path", query: ["c": "d"])
            subject = FrontendRequestMapper()
            outputRequest = subject.map(request: sourceRequest)
        }
        
        it("should have the correct http method") {
            expect(outputRequest.httpMethod) == "GET"
        }
        
        it("should have the correct url") {
            expect(outputRequest.url?.absoluteString) == "http://test.com/path"
        }
        
        it("should have the correct headers") {
            expect(outputRequest.allHTTPHeaderFields! as [String: String]) == ["a": "b"]
        }
        
        it("should have the correct body") {
            let bodyData = outputRequest.httpBody!
            let bodyJson = try! JSONSerialization.jsonObject(with: bodyData, options: []) as? [String: String]
            expect(bodyJson) == ["c": "d"]
        }
        
    }
}
<file_sep>import Foundation

internal class FrontendDownloader {
    
    // MARK: - Properties
    
    internal let session: URLSession
    
    // MARK: - Init
    
    internal init(session: URLSession = URLSession(configuration: URLSessionConfiguration.default)) {
        self.session = session
    }
    
    // MARK: - Internal
    
    internal func download(manifestUrl: String, baseUrl: String, manifestMapper: @escaping ManifestMapper, downloadPath: String, progress: @escaping (_ downloaded: Int, _ total: Int) -> Void, completion: @escaping (Error?) -> Void) {
        self.downloadManifest(manifestUrl: manifestUrl, baseUrl: baseUrl, manifestMapper: manifestMapper) { (manifest, error) in
            if let error = error { completion(error)
            } else if let manifest = manifest {
                self.downloadFiles(manifest: manifest,
                                   downloadPath: downloadPath,
                                   completion: completion,
                                   progress: progress)
            }
        }
    }
    
    // MARK: - Private
    
    fileprivate func downloadFiles(manifest: Manifest, downloadPath: String, completion: (NSError?) -> Void, progress: (_ downloaded: Int, _ total: Int) -> Void) {
        var downloaded: [Manifest.File] = []
        let files: [Manifest.File] = manifest.files
        for file in files {
            // TODO
            /*
             1. If the file is available, copy it
             2. Else, download it

 */
        }
    }
    
    fileprivate func downloadManifest(manifestUrl: String, baseUrl: String, manifestMapper: @escaping ManifestMapper, completion: @escaping (Manifest?, Error?) -> Void) {
        var request: URLRequest = URLRequest(url: URL(string: manifestUrl)!)
        request.addValue("application/json", forHTTPHeaderField: "Accept")
        self.session.dataTask(with: request) { (data, response, error) in
            if let error = error {
                completion(nil, error)
                return
            }
            guard let data = data else { return }
            do {
                let json = try JSONSerialization.jsonObject(with: data, options: [])
                completion(manifestMapper(baseUrl)(json as AnyObject), nil)
            } catch {
                completion(nil, error as NSError)
            }
        }.resume()
    }
    
}
<file_sep>require "rest-client"
require "json"
require 'fileutils'
require 'zip'
require_relative "zip_file_generator"

module Frontend
  class Command

    def initialize(manifest_url, output_zip_file)
      @manifest_url = manifest_url
      @output_zip_file = output_zip_file
    end


    def execute
      begin
        temp_path = File.join(File.expand_path(Dir.pwd), "tmp")
        FileUtils.rm_rf(temp_path)
        FileUtils.mkdir(temp_path)
        manifest_json = self.download_manifest File.join(temp_path, "app.json")
        manifest_json["files"].each do |file|
          self.download_file(file[0], temp_path)
        end
        zip(temp_path)
      ensure
        FileUtils.rm_rf(temp_path)
      end
    end

    protected

    def zip(path)
      puts "==> Zipping to #{@output_zip_file}"
      FileUtils.rm  @output_zip_file, :force => true
      ZipFileGenerator.new(path, @output_zip_file).write
    end

    def download_manifest(save_path)
      puts "==> Downloading manifest: #{@manifest_url}"
      manifest_json = JSON.parse(RestClient.get(@manifest_url))
      manifest_json["base_url"] = self.base_url
      File.open(save_path,"w") do |f|
        f.write(JSON.pretty_generate(manifest_json))
      end
      manifest_json
    end

    def download_file(file_path, save_path)
      puts "===> Downloading file: #{file_path}"
      url = File.join(self.base_url, file_path)
      file_folder = File.join(save_path, file_path)
      file_parent_folder =  file_folder.split("/")[0..-2].join("/")
      FileUtils.mkdir_p file_parent_folder
      File.open(file_folder, 'w') {|f|
        RestClient.get url do |str|
          f.write str
        end
      }
    end

    def base_url
      @manifest_url.split("/")[0..-2].join("/")
    end

  end
end<file_sep>require "thor"
require "frontend/version"
require_relative "frontend/command"

class FrontendCLI < Thor

  desc "download MANIFEST_URL ZIP_PATH", "download the frontend specified in the MANIFEST_URL and compress it into a zip in ZIP_PATH"
  def download(manifest_url, zip_path)
    Frontend::Command.new(manifest_url, zip_path).execute
  end

end

FrontendCLI.start(ARGV)<file_sep>import Foundation
import GCDWebServer

internal class FrontendConfigurator {
    
    // MARK: - Properties
    
    internal let proxyRequestMapper: FrontendProxyRequestMapper
    internal let proxyResources: [ProxyResource]
    internal let requestDispatcher: FrontendRequestDispatcher
    
    // MARK: - Init
    
    internal init(proxyResources: [ProxyResource],
                  proxyRequestMapper: FrontendProxyRequestMapper = FrontendProxyRequestMapper(),
                  requestDispatcher: FrontendRequestDispatcher = FrontendRequestDispatcher()) {
        self.proxyResources = proxyResources
        self.proxyRequestMapper = proxyRequestMapper
        self.requestDispatcher = requestDispatcher
    }
    
    // MARK: - Internal
    
    internal func configure(directoryPath directoryPath: String, server: GCDWebServer) {
        self.configureLocal(directoryPath: directoryPath, server: server)
        self.configureProxy(server: server)
    }
    
    // MARK: - Private
    
    private func configureLocal(directoryPath directoryPath: String, server: GCDWebServer) {
        server.addGETHandler(forBasePath: "/", directoryPath: directoryPath, indexFilename: "index.html", cacheAge: 3600, allowRangeRequests: true)
    }
    
    private func configureProxy(server server: GCDWebServer) {
        server.addHandler(match: { (method, url, headers, path, query) -> GCDWebServerRequest? in
            return self.proxyRequestMapper.map(method: method, url: url, headers: headers, path: path, query: query, proxyResources: self.proxyResources)
            return GCDWebServerRequest()
        }) { (request, completion) in
            self.requestDispatcher.dispatch(request: request!, completion: completion!)
        }
    }
}
<file_sep>import Foundation

public typealias FrontendProgress = ((_ downloaded: Int, _ total: Int) -> Void)
public typealias FrontendCompletion =  ((Error?) -> Void)

@objc open class FrontendController: NSObject {
    
    // MARK: - Attributes
    
    internal let configuration: FrontendConfiguration
    internal let fileManager: FrontendFileManager
    internal let downloader: FrontendDownloader
    internal let server: FrontendServer
    open fileprivate(set) var downloading: Bool = false
    
    // MARK: - Init
    
    convenience public init(configuration: FrontendConfiguration) {
        let fileManager = FrontendFileManager(path: configuration.localPath, zipPath: configuration.zipPath)
        let downloader = FrontendDownloader()
        let server = FrontendServer(path: fileManager.currentPath(), port: configuration.port, proxyResources: configuration.proxyResources)
        self.init(configuration: configuration,
                  fileManager: fileManager,
                  downloader: downloader,
                  server: server)
    }
    
    internal init(configuration: FrontendConfiguration,
                  fileManager: FrontendFileManager,
                  downloader: FrontendDownloader,
                  server: FrontendServer) {
        self.configuration = configuration
        self.fileManager = fileManager
        self.downloader = downloader
        self.server = server
    }
    
    // MARK: - Public
    
    open func setup() throws {
        if !self.fileManager.currentAvailable() {
            try self.fileManager.replaceWithZipped()
        }
        else if self.fileManager.enqueuedAvailable() {
            try self.fileManager.replaceWithEnqueued()
        }
        if !self.fileManager.currentAvailable() {
            throw FrontendControllerError.noFrontendAvailable
        }
        try self.download(replacing: false)
        self.server.start()
    }
    
    open func url() -> String {
        return "http://127.0.0.1:\(self.configuration.port)"
    }
    
    open func available() -> Bool {
        return self.fileManager.currentAvailable()
    }
    
    open func download(replacing replace: Bool, progress: FrontendProgress? = nil, completion: FrontendCompletion? = nil) throws {
        if self.downloading {
            throw FrontendControllerError.alreadyDownloading
        }
        self.downloader.download(manifestUrl: self.configuration.manifestUrl,
                                 baseUrl: self.configuration.baseUrl,
                                 manifestMapper: self.configuration.manifestMapper,
                                 downloadPath: self.fileManager.enqueuedPath(),
                                 progress: { (downloaded, total) in progress?(downloaded, total)
        }) { [weak self] error in
            self?.downloading = false
            if !replace {
                completion?(error)
                return
            }
            do {
                try self?.fileManager.replaceWithEnqueued()
                completion?(nil)
            } catch {
                completion?(error as NSError)
            }
        }
    }
    
}
<file_sep>import Foundation

public struct ProxyResource: Equatable {
    
    // MARK: - Attributes
    
    internal let path: String
    internal let url: String
    
    // MARK: - Init
    
    public init(path: String, url: String) {
        self.path = path
        self.url = url
    }
    
}

public func == (lhs: ProxyResource, rhs: ProxyResource) -> Bool {
    return lhs.path == rhs.path && lhs.url == rhs.url
}<file_sep># Frontend

[![CI Status](http://img.shields.io/travis/<NAME>̃<NAME>́a/Frontend.svg?style=flat)](https://travis-ci.org/<NAME>̃<NAME>́a/Frontend)
[![Version](https://img.shields.io/cocoapods/v/Frontend.svg?style=flat)](http://cocoapods.org/pods/Frontend)
[![License](https://img.shields.io/cocoapods/l/Frontend.svg?style=flat)](http://cocoapods.org/pods/Frontend)
[![Platform](https://img.shields.io/cocoapods/p/Frontend.svg?style=flat)](http://cocoapods.org/pods/Frontend)

## Example

To run the example project, clone the repo, and run `pod install` from the Example directory first.

## Requirements

## Installation

Frontend is available through [CocoaPods](http://cocoapods.org). To install
it, simply add the following line to your Podfile:

```ruby
pod "Frontend"
```

## Author

<NAME>, <EMAIL>

## License

Frontend is available under the MIT license. See the LICENSE file for more info.
<file_sep>import Foundation
import GCDWebServer

internal class FrontendServer {
    
    // MARK: - Attributes
    
    private let path: String
    private let port: UInt
    private let proxyResources: [ProxyResource]
    internal let server: GCDWebServer
    internal let configurator: FrontendConfigurator
    
    // MARK: - Init
    
    internal convenience init(path: String, port: UInt, proxyResources: [ProxyResource]) {
        let server = GCDWebServer()!
        let configurator = FrontendConfigurator(proxyResources: proxyResources)
        self.init(path: path, port: port, proxyResources: proxyResources, server: server, configurator: configurator)
    }
    
    internal init(path: String, port: UInt, proxyResources: [ProxyResource], server: GCDWebServer, configurator: FrontendConfigurator) {
        self.path = path
        self.port = port
        self.proxyResources = proxyResources
        self.server = server
        self.configurator = configurator
    }
    
    // MARK: - Internal
    
    internal func start() -> Bool {
        self.configurator.configure(directoryPath: self.path, server: self.server)
        return self.server.start(withPort: self.port, bonjourName: nil)
    }
    
}
<file_sep>import Foundation
import GCDWebServer

internal class FrontendRequestDispatcher {
    
    // MARK: - Attributes
    
    internal let requestMapper: FrontendRequestMapper
    internal let responseMapper: FrontendResponseMapper
    internal let session: URLSession
    
    // MARK: - Init
    
    internal init(requestMapper: FrontendRequestMapper = FrontendRequestMapper(),
        responseMapper: FrontendResponseMapper = FrontendResponseMapper(),
                  session: URLSession = URLSession(configuration: URLSessionConfiguration.default)) {
        self.requestMapper = requestMapper
        self.responseMapper = responseMapper
        self.session = session
    }
    
    // MARK: - Internal
    
    internal func dispatch(request request: GCDWebServerRequest, completion: @escaping GCDWebServerCompletionBlock) {
        self.session.dataTask(with: self.requestMapper.map(request: request)) { (data, response, error) in
            completion(self.responseMapper.map(data: data, response: response, error: error))
        }.resume()
    }
   
}
<file_sep>import Foundation
import Quick
import Nimble
import GCDWebServer

@testable import Frontend

class FrontendConfiguratorSpec: QuickSpec {
    override func spec() {
        
        var server: MockGCDServer!
        var path: String!
        var subject: FrontendConfigurator!
        var proxyRequestMapper: MockProxyRequestMapper!
        var requestDispatcher: MockRequestDispatcher!
        
        beforeEach {
            server = MockGCDServer()
            path = "path"
            proxyRequestMapper = MockProxyRequestMapper()
            requestDispatcher = MockRequestDispatcher()
            subject = FrontendConfigurator(proxyResources: [], proxyRequestMapper: proxyRequestMapper, requestDispatcher: requestDispatcher)
        }
        
        describe("-configure:directoryPath:server") {
            beforeEach {
                subject.configure(directoryPath: path, server: server)
            }
            describe("local") {
                it("should add a get handler with the correct base path") {
                    expect(server.getHandlerBasePath) == "/"
                }
                it("should add  a get handler with the correct directory path") {
                    expect(server.getHandlerDirectoryPath) == path
                }
                it("should add a get handler with the correct index file name") {
                    expect(server.getHandlerIndexFileName) == "index.html"
                }
                it("should add a get handler with the correct cache age") {
                    expect(server.getHandlerCacheAge) == 3600
                }
                it("should add a get handler with the correct allowRangeRequests") {
                    expect(server.getHandlerAllowRangeRequests) == true
                }
            }
            describe("remote") {
                it("should map the correct method") {
                    expect(proxyRequestMapper.mappedMethod) == "GET"
                }
                it("should map the correct url") {
                    expect(proxyRequestMapper.mappedURL) == URL(string: "url")!
                }
                it("should map the correct headers") {
                    expect(proxyRequestMapper.mappedHeaders as? [String: String]) == [:]
                }
                it("should map the correct path") {
                    expect(proxyRequestMapper.mappedPath) == "path"
                }
                it("should map the correct query") {
                    expect(proxyRequestMapper.mappedQuery as? [String: String]) == [:]
                }
                it("should return the request") {
                    expect(server.returnedRequest).toNot(beNil())
                }
                it("should dispatch the request") {
                    expect(requestDispatcher.dispatchedRequest).to(beIdenticalTo(server.returnedRequest))
                }
            }
        }
    }
}

// MARK: - Mock

private class MockProxyRequestMapper: FrontendProxyRequestMapper {
    
    var mappedMethod: String!
    var mappedURL: URL!
    var mappedHeaders: [AnyHashable: Any]!
    var mappedPath: String!
    var mappedQuery: [AnyHashable: Any]!

    fileprivate override func map(method: String!, url: URL!, headers: [AnyHashable : Any]!, path: String!, query: [AnyHashable : Any]!, proxyResources: [ProxyResource]) -> GCDWebServerRequest? {
        self.mappedMethod = method
        self.mappedURL = url
        self.mappedHeaders = headers
        self.mappedPath = path
        self.mappedQuery = query
        return GCDWebServerRequest(method: "GET", url: URL(string: "url")!, headers: [:], path: "pepi", query: [:])
    }
    
}

private class MockRequestDispatcher: FrontendRequestDispatcher {
    
    var dispatchedRequest: GCDWebServerRequest!
    
    fileprivate override func dispatch(request: GCDWebServerRequest, completion: @escaping GCDWebServerCompletionBlock) {
        self.dispatchedRequest = request
    }

}

private class MockGCDServer: GCDWebServer {
    
    var getHandlerBasePath: String!
    var getHandlerDirectoryPath: String!
    var getHandlerIndexFileName: String!
    var getHandlerCacheAge: UInt!
    var getHandlerAllowRangeRequests: Bool!
    
    var returnedRequest: GCDWebServerRequest!
    
    fileprivate override func addGETHandler(forBasePath basePath: String!, directoryPath: String!, indexFilename: String!, cacheAge: UInt, allowRangeRequests: Bool) {
        self.getHandlerBasePath = basePath
        self.getHandlerDirectoryPath = directoryPath
        self.getHandlerIndexFileName = indexFilename
        self.getHandlerCacheAge = cacheAge
        self.getHandlerAllowRangeRequests = allowRangeRequests
    }
    
    fileprivate override func addHandler(match matchBlock: GCDWebServerMatchBlock!, asyncProcessBlock processBlock: GCDWebServerAsyncProcessBlock!) {
        self.returnedRequest = matchBlock("GET", URL(string: "url")!, [:], "path", [:])
        processBlock(self.returnedRequest, { _ in })
    }
}
<file_sep>import Foundation
import Quick
import Nimble

@testable import Frontend

class FrontendConfigurationSpec: QuickSpec {
    override func spec() {
        
        var manifestUrl: String!
        var baseUrl: String!
        var port: UInt!
        var proxyResources: [ProxyResource]!
        var subject: FrontendConfiguration!
        var zipPath: String!
        
        beforeEach {
            manifestUrl = "manifestUrl"
            baseUrl = "baseUrl"
            port = 8080
            proxyResources = [ProxyResource(path: "path", url: "url")]
            zipPath = "path"
        }
        
        describe("-init") {
            
            beforeEach {
                subject = FrontendConfiguration(manifestUrl: manifestUrl,
                    baseUrl: baseUrl,
                    port: port,
                    zipPath: zipPath,
                    proxyResources: proxyResources)
            }
            
            it("should set the correct manifestUrl") {
                expect(subject.manifestUrl) == manifestUrl
            }
            
            it("should set the correct baseUrl") {
                expect(subject.baseUrl) == baseUrl
            }
            
            it("should have the correct proxyResources") {
                expect(subject.proxyResources) == proxyResources
            }
        
            it("should set the correct localPath") {
                expect(subject.localPath) == FrontendConfiguration.defaultDirectory()
            }
            
            it("should have the correct zipPath") {
                expect(subject.zipPath) == zipPath
            }
        }
        
    }
}<file_sep>import Foundation
import GCDWebServer

internal class FrontendResponseMapper {
    
    internal func map(data data: Data?, response: URLResponse?, error: Error?) -> GCDWebServerResponse {
        let httpUrlResponse = (response as? HTTPURLResponse)
        let contentType = httpUrlResponse?.allHeaderFields["Content-Type"] as? String ?? ""
        var response: GCDWebServerDataResponse!
        if let data = data {
            response = GCDWebServerDataResponse(data: data, contentType: contentType)
        } else {
            response = GCDWebServerDataResponse()
        }
        //TODO: What if Error?
        //TODO: Update the source url?
        if let statusCode = httpUrlResponse?.statusCode {
            response?.statusCode = statusCode
        }
        for header in (httpUrlResponse?.allHeaderFields ?? [:]).keys {
            response.setValue(httpUrlResponse!.allHeaderFields[header] as! String!, forAdditionalHeader: header as! String)
        }
        return response
    }
    
}
<file_sep>import Foundation

public enum FrontendManifestError: Error {
    case invalidManifest(String)
    case unconvertibleJSON
}

public enum FrontendFileManagerError: Error {
    case notAvailable
}

public enum FrontendControllerError: Error, Equatable {
    case alreadyDownloading
    case noFrontendAvailable
}
<file_sep>import Foundation
import GCDWebServer

internal class FrontendRequestMapper {
    
    internal func map(request: GCDWebServerRequest) -> URLRequest {
        var urlRequest: URLRequest = URLRequest(url:  request.url.appendingPathComponent(request.path))
        urlRequest.httpMethod = request.method
        urlRequest.allHTTPHeaderFields = request.headers as? [String: String]
        urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: request.query, options: [])
        return urlRequest
    }
    
}
<file_sep>import Foundation
import SwiftyJSON

public typealias ManifestMapper = (String) -> ((AnyObject) -> Manifest)

internal var DefaultManifestMapper: ManifestMapper = { baseUrl in
    return { input -> Manifest in
        let json = JSON(input)
        let commit = json["commit"].stringValue
        let buildAuthor = json["build_author"].stringValue
        let gitBranch = json["git_branch"].stringValue
        let timestamp = json["timestamp"].intValue
        var files: [Manifest.File] = []
        for element in json["files"].enumerated() {
            let path: String = element.element.0
            let hash: String = element.element.1.stringValue
            let file: Manifest.File = Manifest.File(path: path, hash: hash)
            files.append(file)
        }
        return Manifest(baseUrl: baseUrl,
                        commit: commit,
                        buildAuthor: buildAuthor,
                        gitBranch: gitBranch,
                        timestamp: timestamp,
                        files: files)
    }
}
