Desenvolvimento

15 abr, 2016

Backend usando Swift? Sim, é possível!

Publicidade

Este artigo foi publicado no equinociOS, promovido pelo Cocoaheads-Br.

Um pequeno overview

Recentemente, enquanto preparava uma apresentação sobre “Swift no Backend” para o encontro de desenvolvedores de uma grande empresa, conversei com alguns amigos no Slack e nos corredores da empresa, e percebi que nos dias de hoje temos excelentes desenvolvedores móveis, que por muitas vezes não conhecem outras tecnologias, senão iOS ou Android.

Pessoas que acompanharam a popularização dessas novas tecnologias por meio da Apple e do Google investiram tempo e dinheiro nesse segmento, e deixaram de lado o restante das engrenagens que fazem esse grande relógio chamado World Wide Web funcionar.

Eu mesmo, durante alguns anos, deixei o backend de lado e esquecido, acreditando que parte de um novo futuro estava nas mãos de empresas que estavam surgindo sob uma nova sigla: BaaS (Backend as a Service). Achei que o Parse era a grande solução, quase uma bala de prata. Desenvolvi alguns sistemas usando a ferramenta e não posso dizer que me arrependi.

Depois de um tempo trilhando esse caminho, comecei a perceber quão intrusivo era o Parse. Queria me livrar das garras dele e então renasceu a ideia de me dedicar mais ao backend e buscar alternativas de código aberto, que fossem semelhantes ao que o Parse poderia me oferecer. Nessa busca, tinha em mente algumas coisas: 1) open source, 2) Swift, 3) backend e, é claro, iniciar o próximo projeto em Swift.

Por falta de tempo ou vontade de sair da zona de conforto, esse projeto em Swift não foi iniciado. Finalmente, no início de dezembro de 2015, a Apple liberou o código-fonte do Swift, o que foi ótimo. Em janeiro, conversando com o Ricardo Borelli, percebemos que havia uma vontade mútua de aprender o Swift, mas não para trabalhar no iOS. Queríamos aprender a linguagem de programação, não o framework. A pergunta nesse momento era: “Como?”.

As formas que tínhamos para iniciar nosso aprendizado no Swift eram:

  • Desenvolver uma aplicação iOS (fora do nosso objetivo inicial)
  • Contribuir com projeto open source escrito em Swift

Por termos trabalhado alguns anos como desenvolvedores Backend, a segunda opção era mais atraente, então iniciamos as buscas por projetos open source que nos permitissem desenvolver web applications usando Swift.

Primeiro chegamos ao CocoaPods App. Desencantamos, pois era um projeto com diversas classes escritas em Objective-C. Em seguida, encontramos o Perfect, o Vapor, e então o Ricardo encontrou o Zewo. A partir daí, começou de fato o envolvimento com um projeto 100% open source para servidor, no qual aprenderíamos Swift.

O que é o Zewo?

O Zewo é um conjunto de bibliotecas voltadas para o desenvolvimento de software para servidores. Diversas bibliotecas do Zewo são nada mais do que wrappers de classes, que foram escritas em C.

A história do Zewo

O Zewo nasceu em torno de junho de 2015, na época em que a Apple anunciou que abriria o código-fonte do Swift. O primeiro nome dele foi SwiftHTTPServer e foi construído pelo Paulo Faria – um dev brasileiro -, que manteve a construção do SwiftHTTPServer em constante crescimento, até pouco antes de a Apple finalmente liberar o código-fonte do Swift, quando ele decidiu reiniciar tudo. E então surgiu o Zewo com uma proposta diferente: “não ser apenas um web framework, mas uma plataforma de desenvolvimento para tudo que fosse relacionado a servidor”. E tudo isso graças ao fato de compilar para Linux.

Se é uma plataforma para servidores, me dá uns exemplos do que eu posso fazer com o Zewo

Você pode trabalhar com praticamente tudo que você quiser no servidor. Alguns poucos exemplos:

  • Crawlers
  • Loggers
  • Renders
  • Database (MySQL e Postgres)
  • Scripts
  • Web Applications
  • Conexões remotas usando TCP/UDP

E o mais interessante: sem precisar da Foundation, que ainda não possui o código-fonte liberado.

Zewo + Swift X

Desde o início, modularização foi um dos focos principais do Zewo. Isso faz com que a reutilização de código ocorra de forma muito mais fácil. Quando Vapor (um dos frameworks web para Swift) resolveu se integrar aos módulos do Zewo, surgiu a ideia de criar um padrão para compartilhamento de código entre projetos Swift. Nasceu então o Swift X (Swift Cross Project Standards). O Swift X é uma coleção de protocolos e tipos básicos, passando por dados binários, requisições e respostas HTTP, URIs, até alcançar um padrão para servidores e clientes HTTP, roteadores HTTP, middlewares etc.

O Swift X ainda é bem novo, mas já tem alguns frameworks iniciando o suporte para seus padrões. A próxima versão do Zewo já vai fazer uso do SX, facilitando ainda mais o compartilhamento de código e integração entre a comunidade Swift.

Vamos começar?

Você precisa de algumas ferramentas. Vâmo lá, receita de bolo ?:

Depois de instalado, será necessário adicionar a fórmula do Zewo para o Homebrew. No terminal, digite o seguinte comando

$ brew tap zewo/tap
$ brew install zewo

Toolchain

Para iniciar os trabalhos de forma simples, usando o Mac OS X, precisaremos do toolchain do Swift.

Se você já tem tudo isso instalado, vá direto para Criando o Hello World.

Faça o download do toolchain de 08 de fevereiro em https://swift.org/download/#using-downloads

swift-download-page

Finalizado o download, execute o swift-DEVELOPMENT-SNAPSHOT-2016-02-08-a-osx.pkg.

Será necessário que você adicione à sua variável de ambiente o path do toolchain que foi instalado.

Abra o terminal e digite o seguinte comando

$ open ~/.bash_profile

Adicione o path padrão de instalação do Swift (caso você não tenha alterado o diretório de instalação).

/Library/Developer/Toolchains/swift-latest.xctoolchain/usr/bin

O resultado final da variável de ambiente PATH deverá ser algo semelhante ao screenshot. Salve a modificação no .bash_profile.

environment-variable-path-textedit

Feito isso, será necessário executar o .bash_profile, usando o comando abaixo

$ source ~/.bash_profile

Criando um Hello World

Vamos voltar ao terminal

$ cd ~/
$ mkdir hello-world
$ cd hello-world
$ swift-build --init
Creating Package.swift
Creating .gitignore
Creating Sources/
Creating Sources/main.swift
Creating Tests/

Com um editor de texto, edite o arquivo Package.swift e adicione os repositórios HTTPServer, Router e LogMiddleware à nossa lista de dependências.

Package.swift

import PackageDescription

let package = Package(
    name: "hello-world",
    dependencies: [
        .Package(url: "https://github.com/Zewo/HTTPServer.git", majorVersion: 0, minor: 3),
        .Package(url: "https://github.com/Zewo/Router.git", majorVersion: 0, minor: 3),
        .Package(url: "https://github.com/Zewo/LogMiddleware.git", majorVersion: 0, minor: 3)
    ]
)

Sources/main.swift

import HTTPServer
import Router
import LogMiddleware

let log = Log()
let logMiddleware = LogMiddleware(log: log)

let router = Router(middleware: logMiddleware) { route in
    route.get("/") { request in
        return Response(body: "Hello World!")
    }
}

try Server(port: 8080, responder: router).start()

Após implementar essas milhares poucas linhas de código, volte ao terminal, no diretório do projeto, e execute o compilador do Swift.

$ swift-build

Na primeira vez que você executar o swift-build no diretório do seu projeto, o Swift Package Manager fará o clone dos projetos que adicionamos na chave dependencies, no Package.swift e suas subdependências – em seguida, os sources serão compilados. Por fim, o resultado será um executável gerado pelo compilador.

$ swift-build
Compiling Swift Module 'helloworld' (1 sources)
Linking Executable:  .build/debug/hello-world

Agora que o compilador disse pra gente o que compilou e onde gerou o executável, fica fácil saber o que executar.

Assumindo que você está no diretório hello-world, digite o seguinte no terminal:

$ .build/debug/hello-world



                             _____
     ,.-``-._.-``-.,        /__  /  ___ _      ______
    |`-._,.-`-.,_.-`|         / /  / _ \ | /| / / __ \
    |   |ˆ-. .-`|   |        / /__/  __/ |/ |/ / /_/ /
    `-.,|   |   |,.-`       /____/\___/|__/|__/\____/ (c)
        `-.,|,.-`           -----------------------------

================================================================================
Started HTTP server, listening on port 8080.

Agora que temos o nosso servidor rodando devidamente no terminal, abra o browser e digite a seguinte URL: http://127.0.0.1:8080. Você verá algo como o screeshot abaixo:

hello-world-running-on-browser

Por ter configurado o LogMiddleware no Package.swift, as requisições disparadas contra o servidor serão mostradas no console.

Em diversos web frameworks, mostrar os detalhes da requisição no console é parte da configuração padrão. Como no Zewo separamos todos os módulos, deixamos essa configuração como opção para o usuário. No Hello World, não estamos persistindo esses dados, mas, caso seja interessante, você pode usar o File para guardá-los em um arquivo.

hello-world-showing-log-on-console

Enfim, depois de tanta explicação, esse é o nosso Hello World usando Swift e Zewo. Por que não incrementar um pouco mais esse exemplo ? Fique tranquilo que a partir de agora não serei tão detalhista! Será? ?

Criando um render html

Legal, bacana, temos o Hello World funcionando. Mas se estamos falando sobre montar uma web application, então é necessário renderizar um HTML com informações vindas do servidor, não é mesmo ? Por isso, vamos usar o Sideburns – uma pequena camada sobre o Mustache, que é um fork do GRMustache – e o HTTPFile, que é a implementação do protocolo ResponderType e será usado para expor o conteúdo da pasta public.

Ao adicionar o Sideburns à lista de dependencies, o Package.swift deverá ficar assim:

import PackageDescription

let package = Package(
    name: "hello-world",
    dependencies: [
        .Package(url: "https://github.com/Zewo/HTTPServer.git", majorVersion: 0, minor: 3),
        .Package(url: "https://github.com/Zewo/Router.git", majorVersion: 0, minor: 3),
        .Package(url: "https://github.com/Zewo/LogMiddleware.git", majorVersion: 0, minor: 3),
        .Package(url: "https://github.com/Zewo/HTTPFile.git", majorVersion: 0, minor: 3),
        .Package(url: "https://github.com/Zewo/Sideburns.git", majorVersion: 0, minor: 3)
    ]
)

Show! Logo após adicionar os novos pacotes, o ideal seria voltar ao terminal e rodar o swift-build, mas podemos esperar um pouquinho mais e fazer tudo de uma vez. Mas se paciência não for muito seu forte, manda ver! Mal não vai fazer! ?

Na raiz do seu projeto, vamos criar uma pasta chamada public e vamos adicionar alguns arquivos simples. No terminal digite:

$ mkdir -p public/assets

Crie um arquivo main.css dentro da pasta assets:

$ touch public/assets/main.css

Edite o main.css e vamos adicionar uma simples propriedade, apenas para que a nossa página não fique completamente sem estilo.

body {
    font-family: "verdana";
    font-size: 10px;
}

Crie na raiz da pasta public um arquivo chamado hello.html.

$ touch public/hello.html

Edite o hello.html e adicione o seguinte bloco ao arquivo.

Caso você tenha dúvidas sobre a forma de trabalhar com o Mustache, acesse a documentação da ferramenta no link https://github.com/groue/GRMustache.

<html>
    <head>
        <title>{{ title }}</title>
        <meta charset="UTF-8">
        <link rel="stylesheet" type="text/css" href="/assets/main.css">
    </head>
    <body>
        <div>
            <h1>
            {{ description }}
            </h1>
            <ul>
                {{# messages }}
                <li> {{ author }} - {{ message }} </li>
                {{/ messages }}
            </ul>
        </div>
    </body>
</html>

O que adicionaremos agora ao nosso Sources/main.swift:

  • Estrutura de dados para ser renderizado no html, que será o response do endpoint “/”.
  • Response com path do arquivo que será usado para renderizar o dicionário que acabamos de adicionar.
  • FileResponder, que será responsável por expor os arquivos da pasta public de forma estática.

O resultado final do nosso main.swift:

import HTTPServer
import Router
import LogMiddleware
import HTTPFile
import Mustache
import Sideburns

let log = Log()
let logMiddleware = LogMiddleware(log: log)

let router = Router(middleware: logMiddleware) { route in
    route.get("/") { request in
        let data: [String: Any] = [
            "title": "Título da Página",
            "description": "Renderizando HTML usando Sideburns",
            "messages": [ 
                [
                    "author": "Patrick", 
                    "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
                ],
                [
                    "author": "Senhor Sirigueijo", 
                    "message": "Morbi luctus urna vel lacus malesuada posuere."
                ],
                [
                    "author": "Lula Molusco", 
                    "message": "Praesent dignissim nunc a convallis posuere."
                ],
                [
                    "author": "Bob Esponja", 
                    "message": "Nam facilisis arcu at consequat sagittis."
                ]
            ]
        ]
        return try Response(templatePath: "public/hello.html", templateData: data)
    }
    route.fallback = FileResponder(basePath: "public")
}

try Server(port: 8080, responder: router).start()

Ok, terminamos de incrementar o nosso Hello World com a renderização de algumas informações do servidor. Nesse caso, os dados estão todos “mockados”, mas poderiam vir diretamente de um banco (que é assunto para um próximo artigo ?). Agora vamos compilar as nossas mudanças e executar o binário gerado pelo compilador. Para isso, volte ao terminal e execute

$ swift-build
$ .build/debug/hello-world

Abra o browser e digite o endereço http://127.0.0.1:8080. Ao carregar a tela, esse deve ser o resultado, e o html será parecido com que está listado abaixo:

<html>
    <head>
        <title>Título da Página</title>
        <meta charset="UTF-8">
        <link rel="stylesheet" type="text/css" href="/assets/main.css">
    </head>
    <body>
        <div>
            <h1>
            Renderizando HTML usando Sideburns
            </h1>
            <ul>
                <li> Patrick - Lorem ipsum dolor sit amet, consectetur adipiscing elit. </li>
                <li> Senhor Sirigueijo - Morbi luctus urna vel lacus malesuada posuere. </li>
                <li> Lula Molusco - Praesent dignissim nunc a convallis posuere. </li>
                <li> Bob Esponja - Nam facilisis arcu at consequat sagittis. </li>
            </ul>
        </div>
    </body>
</html>

E, por aqui, finalizamos mais um exemplo de como podemos renderizar valores em um html.

Criando uma API simples

Agora, vamos usar um repositório que já tem como dependência diversos outros que precisamos para web applications – o Zewo -, um repositório que podemos chamar de umbrella package, fazendo uma referência a umbrella header.

Por conta de diversas libraries ainda estarem sendo escritas, principalmente no caso de rotas RESTful, neste artigo veremos apenas as rotas de consulta mais simples. Em breve, escreverei outro abordando APIs mais complexas, prometo ??!

Nesse exemplo, criaremos uma API ridiculamente simples. O objetivo não é ser complexo, e por isso não faria sentido criar muitos métodos.

Então vamos começar criando um novo pacote para a nossa API? De volta ao terminal, crie uma nova pasta com o nome todo-api:

$ cd ~/
$ mkdir todo-api
$ cd todo-api
$ swift-build --init
Creating Package.swift
Creating .gitignore
Creating Sources/
Creating Sources/main.swift
Creating Tests/

Vamos editar o arquivo Package.swift e adicionar a única dependência que precisaremos, o Zewo.

Se você quiser conhecer quais são as dependências usadas por esse repositório, acesse esta URL: https://github.com/Zewo/Zewo/blob/master/Package.swift

import PackageDescription

let package = Package(
    name: "todo-api",
    dependencies: [
        .Package(url: "https://github.com/Zewo/Zewo", majorVersion: 0, minor: 3)
    ]
)

Agora nós precisamos criar de fato as nossas rotas e respostas; sendo assim, edite o arquivo Sources/main.swift e cole o conteúdo do bloco abaixo

import HTTPServer
import Router
import LogMiddleware
import HTTPFile
import ContentNegotiationMiddleware
import LogMiddleware
import JSONMediaType
import InterchangeData

let log                 = Log()
let logMiddleware       = LogMiddleware(log: log)
let contentNegotiaton   = ContentNegotiationMiddleware(mediaTypes: JSONMediaType())

let router = Router(middleware: logMiddleware, contentNegotiaton) { route in
    
    var items: [InterchangeData] = [
        ["id": "1", "description": "Comprar frutas"],
        ["id": "5", "description": "Pagar condomínio"],
        ["id": "3", "description": "Despachar encomenda"],
        ["id": "6", "description": "Assistir Deadpool"],
        ["id": "4", "description": "Ir ao banco"],
        ["id": "2", "description": "Ligar para operadora"]
    ]
    
    func get(id: String) throws -> InterchangeData {
        let list = items.filter({ $0["id"]!.string == id })

        guard let todo = list.first else {
            return nil
        }
        
        return todo
    }
    
    // cria o novo registro de uma tarefa
    route.post("/todo") { request in
        let body        = request.content!
        
        let id          = InterchangeData.from(String(arc4random()))
        let description = body["description"]
        
        let todo: InterchangeData = [
            "id"          : id,
            "description" : description!
        ]
        
        items.append(todo)
        
        return Response(status: .Created, content: todo)
    }
    
    // Lista todas os registros de tarefas
    route.get("/todo") { request in
        items = items.sort({ $0["id"]!.string < $1["id"]!.string })
        
        guard items.count > 0 else {
            return Response(status: .NoContent)
        }
        
        return Response(content: InterchangeData.from(items))
    }
    
    // busca os dados de uma tarefa baseado no id
    route.get("/todo/:id") { request in
        let path = request.pathParameters
        
        let todo = try get(path["id"]!)
        guard todo != nil else {
            return Response(status: .NotFound)
        }
        
        return Response(content: todo)
    }
    
    // atualiza uma tarefa baseado no id
    route.put("/todo/:id") { request in
        let path = request.pathParameters
        let body = request.content!
        
        var todo = try get(path["id"]!)
        guard todo != nil else {
            return Response(status: .NotFound)
        }
        
        let index = items.indexOf(todo)!
        todo["description"] = body["description"]
        items[index] = todo
        
        return Response(status: .NoContent)
    }
    
    // exclui uma tarefa baseado no id
    route.delete("/todo/:id") { request in
        let path = request.pathParameters
        
        let todo = try get(path["id"]!)
        guard todo != nil else {
            return Response(status: .NotFound)
        }
        
        let index = items.indexOf(todo)!
        guard items.removeAtIndex(index) != nil else {
            return Response(status: .NotFound)
        }
        
        return Response(status: .NoContent)
    }
}

try Server(port: 8080, responder: router).start()

É claro que você, um expert em Swift, pode criticar diversos pontos do bloco acima, infelizmente ainda não faço parte desse seleto time, mas estamos a caminho.

Nesse exemplo, criamos:

  • Criação de uma tarefa
  • Listagem das tarefas
  • Detalhe de uma tarefa
  • Atualização de uma tarefa
  • Exclusão de uma tarefa

Em princípio, para que não ficasse com mais dependências (ex.: banco de dados), usamos um array mutável para persistir e listar as tarefas. É claro que essa não chega nem a ser uma forma decente de persistência, mas atende ao nosso propósito educativo.

ContentNegotiationMiddleware

Esse middleware tem como função básica o tratamento do tipo de informação recebida no request. É baseado no Content-Type e no Accept. No nosso exemplo, trabalharemos apenas com JSON. Caso haja necessidade, você pode optar por usar o URLEncodedForm.

Depois de rodar o exemplo anterior, você pode estar perguntando: como proteger a nossa humilde API? Bom, temos à disposição diversas ferramentas para nos auxiliar nesse ponto, mas, por enquanto, apenas uma está implementada de forma nativa no Zewo: o BasicAuthMiddleware (RFC 2617).

Infelizmente, o HTTP Basic Authentication não é seguro em relação ao dado trafegado. Encriptar os dados requer Base64, uma encriptação de duas vias, ou seja, a segurança é tão fraca que qualquer pessoa que interceptar a sua conexão com o servidor facilmente descobrirá qual usuário e senha está sendo transmitido. Se você usa HTTPS, a vida de quem está tentando interceptar os dados trafegados complica um pouco mais.

Como adicionar o BasicAuth às minhas rotas?

Em outras ocasiões, você precisaria adicionar o pacote BasicAuthMiddleware à lista de dependências do Package.swift, mas nós estamos usando o Zewo, que é o nosso umbrella package e ele já possui essa dependência. Então, em vez de trabalhar com diversas dependências, nós vamos voltar a editar o Sources/main.swift.

Importe o BasicAuthMiddleware no topo do main.swift.

import BasicAuthMiddleware

Vamos instanciar o BasicAuthMiddleware:

import BasicAuthMiddleware

E configurar no nosso Router. Basta adicionar a variável basicAuth ao parâmetro middleware, e deve ficar assim:

let basicAuth = BasicAuthMiddleware { username, password in
    if username == "admin" && password == "password" {
        return .Authenticated
    }

    return .AccessDenied
}

O que precisávamos para “segurar” a nossa API era basicamente desses dois passos. O resultado disso é simples: se você não informar as credenciais no momento da requisição, a API recusará o seu acesso.

api-secured-without-credentials

Ao informar credenciais válidas, a API devolverá os dados normalmente e efetuará as devidas ações, como: insert, update, delete e quantos mais você configurar.

E, finalmente, terminamos por aqui o terceiro e último exemplo do nosso artigo.

Considerações finais

Como você pode ver, o desenvolvimento de web applications não é realmente um monstro de sete cabeças como talvez você estivesse imaginando. É claro que no desenvolvimento de software tudo pode piorar, e pode apostar que vai. Com um pouco de estudo, dedicação, fonte de pesquisa (tá bom, Stack Overflow vale também ?) e uma boa documentação, você pode e DEVE ir longe.

Durante o tempo em que estive escrevendo este artigo, a Apple liberou mais um toolchain com diversas novas possibilidades que, com toda certeza, nos ajudarão muito daqui pra frente. Pelo fato de agora o Swift também compilar C, podemos fazer deploy das aplicações escritas em Swift para o Heroku, mas isso também ficará para uma próxima vez.

Os códigos dos exemplos estão todos no meu perfil do Github, na URL https://github.com/unnamedd/equinocios-backend.

Este artigo teve a participação direta de diversos amigos, sugerindo, corrigindo ou ensinando.

Meus agradecimentos ao Paulo Faria, ao Ricardo Borelli e à minha amada Carolina.