Extensões para o Rung

Rung CLI logo

Aprenda a desenvolver extensões para o Rung.

Definição

O que é o Rung

O Rung é a primeira ferramenta do mundo para gestão de excepcionalidades. O objetivo da ferramenta é te alertar de situações que podem ser importantes para você ou para sua empresa. O Rung é altamente customizável. Há uma loja de extensões que permite a você configurar o que quer monitorar, e também lhe permite programar suas próprias extensões.

O que é uma extensão

Uma extensão é um aplicativo para o Rung, que foi previamente programado para receber uma série de parâmetros e, quando houver a satisfação de uma condição específica, gerar alertas e notificar o usuário sobre. Um exemplo simples seria a cotação do dólar. Por exemplo, eu posso querer ser avisado quando o dólar estiver abaixo de R$ 3,00.

Características

Contexto

O contexto é o primeiro parâmetro passado para a função principal da sua extensão. Ele traz informações importantes, como o locale do usuário e os valores dos parâmetros que são informados.

Exemplo:

import { create } from 'rung-sdk';
import { Integer } from 'rung-sdk/dist/types';

function main(context) {
    const { a, b } = context.params;

    return {
        alerts: [`Sum of ${a} and ${b} is ${a + b}`]
    };
}

const params = {
    a: {
        description: 'First number',
        type: Integer
    },
    b: {
        description: 'Second number',
        type: Integer
    }
};

export default create(main, { params });

O contexto também é responsável por carregar as informações do banco de dados de cada instância de extensão, em context.db.

Chaves primárias

Cada alerta pode ter uma chave primária. A chave primária serve para localizar aquele alerta e o preservar em uma atualização. É indiciado que todo alerta possua uma chave primária para que seus metadados não sejam perdidos durante cada execução da extensão.

Para habilitar o suporte às chaves primárias, deve-se utilizar o segundo parâmetro da função create da SDK do Rung:

export default create(main, {
    params,
    primaryKey: true
});

Por padrão, os alertas devem ser retornados em um objeto contendo { alerts }. Nessa situação, alerts deve conter um objeto ou array com as chaves e os valores de cada alerta. No caso de um array, a chave utilizada será o índice. Com ES6, você pode construir um objeto com chaves dinâmicas utilizando a sintaxe { [key]: value }. Exemplo:

import { map, mergeAll } from 'ramda';

function render({ id, name, score }) {
    return { [id]: `${name} with ${score} points!` };
}

function main(context) {
    const students = [
        { id: '0001', name: 'Bianca del Rio', score: 8 },
        { id: '0002', name: 'Violet Chachki', score: 9 },
        { id: '0003', name: 'Dida Ritz', score: 3 }
    ];

    return {
        alerts: mergeAll(map(render, students))
    };
}

export default create(main, { params: {}, primaryKey: true });

Extensões assíncronas

O Rung possui dois tipos de chamada de retorno para a função principal. Uma extensão pode ser síncrona ou assíncrona. Uma extensão síncrona executa linha a linha e tem seu valor processado retornado. É o caso, por exemplo, de uma extensão que parabenize o indivíduo no seu aniversário. Quando uma extensão precisa “esperar” uma resposta, temos o caso de uma extensão assíncrona. Exemplos são requisições HTTP.

Para diferenciar extensões síncronas e assíncronas, utilizamos um segundo parâmetro na função principal, o done. Quando o done é passado, a extensão não termina de executar até que a função done seja chamada com os alertas a serem gerados. Na outra situação, a extensão termina sua execução até quando o primeiro return for encontrado.

Extensão síncrona

function main(context) {
    return {
        alerts: ["Hello world!"]
    }
}

Extensão assíncrona

function main(context, done) {
    setTimeout(() => {
        done({
            alerts: ["Hello world!"]
        });
    }, 3000);
}

No segundo exemplo, a extensão leva 3 segundos para executar e então gerar o alerta Hello world!.

Tipos de parâmetros

Os parâmetros de entrada de uma extensão do Rung possuem diversos tipos com múltiplas finalidades, que variam de acordo com como você vai trabalhar com o dado e como você deseja que ele seja renderizado para o usuário durante a instalação. Os tipos podem ser importados via:

import {} from 'rung-cli/dist/types';

Integer

Para números inteiros tendendo de negativo a infinito positivo, a depender da representação de números inteiros em bits de onde está rodando.

Double

Para números de ponto flutuante (decimais), a depender da representação de números de ponto flutuante em bits de onde está rodando, geralmente segundo a IEEE754.

DateTime

Indisponível

Natural

Números naturais de 0 a infinito. Não são aceitos números negativos

Char(n)

Textos de até n caracteres, onde n corresponde a um número natural. Se tiver como entrada um texto maior, este é cortado até o limite.

IntegerRange(m, n)

Intervalos numéricos de números inteiros entre m e n. É possível, por exemplo, simular números naturais com IntegerRange(0, Infinity). m sempre precisa ser menor do que n para que haja uma situação válida.

DoubleRange(m, n)

Intervalos numéricos de números de ponto flutuante (decimais) entre m e n, onde m precisa ser um valor menor que n.

Money

Indisponível

String

Qualquer trecho de texto arbitrário

Color

Cor em hexadecimal, começando por #.

Email

Qualquer email válido.

Checkbox

Indisponível

OneOf(xs)

Qualquer elemento de xs, onde xs deve ser do tipo string[] (lista de strings).

Url

Url independente de protocolo (HTTP, FTP …).

IntegerMultiRange(m, n)

Slider em que 2 pontos são retornados, estão esses pontos entre m e n.

Calendar

Para datas, com renderização de um calendário.

AutoComplete

Para campos que podem ser interdependentes, em modo texto e ter um arquivo expondo uma função de autocomplete.

Location

Qualquer trecho de texto que faça referência a uma localização (cidade, estabelecimento, país, etc). Quando o campo for utilizado no Rung, irá ser carregado um autocomplete com sugestões de locais vindas do Google.

Banco de dados

Também há suporte para micro esquema de banco de dados não-relacional. Cada extensão pode armazenar um objeto JS que possa ser representado como JSON. Esse objeto pode ser retornado juntamente com os alertas da extensão. Durante a primeira execução, o valor sempre corresponde a undefined.

Exemplo de contador

import { create } from 'rung-sdk';

function main(context) {
    const counter = context.db === undefined ? 0 : context.db;

    return {
        alerts: [`The value is ${counter}`],
        db: counter + 1
    };
}

export default create(main, { params: {}, primaryKey: true };

Cada vez que a extensão roda, o contador é incrementado. Caso ele seja undefined, é inicializado como 0. Você pode colocar objetos de complexidade bem maiores dentro dele.

Para mais informações sobre a utilização com o Rung CLI, visite db.

Internacionalização e tradução

O Rung tem suporte a múltiplos idiomas nas extensões. Os locales do Rung se baseiam na combinação por _ dos formatos ISO 639 com o código do país, especificado na ISO 3166. Exemplos são en_US e pt_BR.

Há uma função global chamada _, que recebe um texto a ser internacionalizado, preferencialmente em inglês, e opcionalmente um conjunto de pares a interpolar. O Rung buscará por arquivos .json com o nome do locale dentro da pasta locales/. Exemplo:

locales/pt_BR.json

{
    "Hello world!": "Olá, mundo!"
}

locales/zh_CN.json

{
    "Hello world!": "你好世界!"
}

index.js

function main(context) {
    return {
        alerts: [_("hello world!")]
    };
}

Caso o locale não for encontrado, a chave é utilizada, no caso, a versão em inglês no exemplo.

Interpolação de strings

A função _ aceita um segundo parâmetro para interpolar, tornando possível fazer, por exemplo:

return {
    alerts: [_("good morning, {{name}}!", { name: 'Trixie Mattel' })]
};

Note que, no arquivo .json, os nomes dentro das chavetas devem ser preservados

Cards personalizados

Além de texto plano, o Rung permite que os cards possam ter um subset de HTML personalizado para cada extensão. O compilador de extensões do Rung possui suporte direto a JSX, permitindo que o conceito do React seja aplicado aos cards em tempo de compilação.

_images/custom-cards.png

JSX

Ao invés de definir o HTML como string, utilize-o diretamente na renderização. Também há suporte a folha de estilos como objetos dentro das tags Exemplo de extensão customizada:

import { create } from 'rung-sdk';

const styles = {
    name: {
        fontWeight: 'bold',
        color: 'red'
    }
};

function render(name) {
    return (
        <div>
            Hello, <span style={ styles.name }>{ name }</span>
        </div>
    );
}

function main(context) {
    return {
        alerts: [{
            title: 'Betty',
            content: render('Betty')
        }]
    };
}

export default create(main, { params: {}, primaryKey: true });

Preview

É ideal especificar como o card será exibido para o usuário durante a instalação da extensão dentro do Rung App. Para isso, você pode definir nas configurações o parâmetro preview e reutilizar a função render:

-export default create(main, { params: {}, primaryKey: true });
+export default create(main, {
+    params: {},
+    primaryKey: true,
+    preview: render('Salete')
+});

Hot reloading

A partir da versão 1.1.1, o Rung CLI suporta desenvolvimento agilizado através de edição ao vivo e compilação dinâmica. Se você está habituado a desenvolver com React, vai se sentir familiarizado.

_images/live-preview.png

Como configurar

Para habilitar o modo live, rode sua extensão com rung run --live, e preencha os valores dos parâmetros de entrada. Nada mais é necessário. O Rung CLI irá monitorar as mudanças e compilar dinamicamente!

_images/live-console.png

Linha do tempo

Como o estado é puro, uma linha do tempo é criada e você pode navegar pelos estados anteriores. Quando uma mudança é detectada, uma nova era é criada e a aplicação é reposicionada para ela.

_images/live-timeline.png

Caso você interrompa o processo e entre com novos parâmetros, não é necessário fechar o navegador ou recarregar a página. O mesmo é válido para quando executar outra extensão. As novas entradas serão adicionadas à linha do tempo das anteriores.

Erros

Erros sintáticos e de runtime serão exibidos em uma caixa específica.

_images/live-error.png

Barra lateral

Você pode clicar nos alertas para ver o resultado da renderização do markdown para comentários na barra lateral.

Barra lateral personalizada

Usando a propriedade sidebar nas configurações, é possível escolher quais campos deseja ocultar. Por padrão, a barra lateral possui suporte a 4 campos:

Identificador Descrição
priority Prioridade
situation Situação
startDate Data inicial
endDate Data final
_images/sidebar.png

Se quisermos, por exemplo, remover os campos de data:

export default create(main, {
    params,
    primaryKey: true,
    sidebar: {
        startDate: false,
        endDate: false
    }
});
_images/custom-sidebar.png

Ícone personalizado

Ao publicar uma extensão, o Rung tentará localizar um arquivo icon.png no seu pacote. Ele corresponde à identidade de sua extensão. Não é obrigatório, mas é recomendado. Ele determina como sua extensão será mostrada na Rung Store:

_images/store.png

Rung Bot

Em seus alertas, é possível programar para que haja um comentário no follow-up do Rung Bot, que é, basicamente, um robô customizado que tem o objetivo de complementar a informação do card.

_images/bot.png

O conteúdo escrito pelo Rung Bot deve ser definido usando Markdown, e paralelo ao card customizado. Exemplo:

return {
    alerts: [{
        title: 'Bananas are cheap',
        content: render(bananasPrice),
        comment: renderComment(bananasPrice)
    }]
};
function renderComment(bananasPrice) {
    return `
        # Bananas are cheap!
         The bananas are costing U$ ${bananasPrice}!!!
        Buy them now!
        ![Bananas](https://bananas.org/banana.jpg)
    `;
}

Resources

Quando um alerta de uma integração possui uma ou várias imagens, pode-se utilizar a propriedade resources. Esta propriedade permite que as imagens sejam visualizadas em um único comentário e em forma de carrossel, deixando os comentários mais limpos e objetivos.

_images/resources.png

O conteúdo passado para a propriedade resources deve ser uma lista contendo as urls das imagens geradas pelo alerta. Exemplo:

return {
    alerts: [{
        title: 'Bananas are cheap',
        content: render(bananasPrice),
        comment: renderComment(bananasPrice),
        resources: [
           'http://www.bananas.com/banana1.jpg',
           'http://www.bananas.com/banana2.jpg',
           'http://www.bananas.com/banana3.jpg'
        ]
    }]
};

AutoComplete

O Rung suporta que os campos possam ter a função de autocompletar, provendo uma função JavaScript que lida com o retorno baseado na entrada.

Para que um campo seja autocompletável, é necessário defini-lo como um campo do tipo AutoComplete, disponível em rung-cli/dist/types.

Vamos criar um campo que tenha o autocompletar com nomes de Pokémons nesse exemplo!

  • Defina o tipo do campo como AutoComplete
params: {
    pokemon: {
        type: AutoComplete,
        description: 'Pick a Pokémon!'
    }
}
  • Crie um arquivo autocomplete/pokemon.js
  • Exporte uma função que use a função de callback ou que retorne uma lista de strings
export default function ({ input, lib }) {
     return lib.request.get('https://raw.githubusercontent.com/BrunnerLivio/PokemonDataGraber/master/output.json')
        .then(({ text }) => JSON.parse(text))
        .then(pokemons => pokemons.map(pokemon => pokemon.Name))
        .filter(name => name.startsWith(input));
}

E pronto, compile e suba sua extensão para o Rung! Você também pode testar essa funcionalidade na sua extensão diretamente do Rung CLI (automaticamente).

Parâmetros

A função exportada recebe como parâmetros:

  1. Objeto contendo input (entrada do usuário) e lib
  2. Callback opcional done. Se passado, deve ser chamado para retornar o controle. Senão, a função deve retornar uma Promise

Retorno

Promise ou chamada de done a um Array<String> contendo os dados já filtrados.

Bibliotecas

O parâmetro lib vindo dentro do primeiro objeto carrega duas bibliotecas, ramda e request. As requisições são feitas utilizado a biblioteca externa superagent.

Informações adicionais

Atualmente, o Rung possibilita que o desenvolvedor defina, além da descrição, um conteúdo adicional (no formato markdown) a ser apresentado na página da sua extensão na loja. Este conteúdo adicional é opcional, mas é recomendado o seu uso de tal forma que o seu conteúdo instigue o usuário a instalar e utilizar a extensão.

O conteúdo adicional deve ser estruturado dentro da pasta info e pode ser definido em inglês (en), português (pt_BR) e espanhol (es), conforme imagem abaixo.

_images/info.png

Rung CLI

Compilando uma extensão

Você pode usar o comando rung build para compilar uma extensão para a forma binária, gerando um arquivo .rung. O binário gerado pode então ser usado para distribuição e publicação para a loja do Rung, mas não precisa ser gerado para rodar uma extensão localmente, levando em conta que o Rung CLI consegue atuar diretamente como um interpretador.

O binário do Rung é uma forma modificada de um arquivo PKZip. Em tempo de compilação, a VM do Rung executa sua extensão para garantir que não há erros de análise do código exportado e gera os metadados para sua extensão de forma estática, para otimizar a execução.

Executando uma extensão

É possível testar as extensões que desenvolve locamente sem a necessidade de publicá-las para o Rung. O Rung CLI provê o comando rung run para isso.

_images/run.png

É criada uma interface visual que recebe os parâmetros de entrada e mostra o resultado obtido, os alertas que seriam gerados, para o programador.

Executando com locale customizado

Quando estiver testando internacionalização, pode forçar a definição de um locale pelo próprio terminal, executando:

RUNG_LOCALE=pt_BR rung run

Por padrão, sem essa definição, o Rung CLI irá considerar o locale padrão do seu sistema operacional.

Publicação

Após o desenvolvimento, podemos publicar a extensão para a Rung Store. O Rung CLI implementa o comando rung publish para isso. Ao publicar uma extensão, informe seu usuário e senha do Rung.

Restrições de publicação

  • A versão deve atender ao semver;
  • Ao atualizar uma extensão, a versão deve ser incrementada;
  • É necessário possuir karma de desenvolvedor.

Para conseguir karma de publicação, entre em contato conosco em developer@rung.com.br.

Para publicar uma extensão, é obrigatório que title e description estejam definidos na configuração da extensão.

Boilerplate

O comando rung boilerplate pede algumas informações sobre a sua extensão e gera a base funcional de código para que você possa trabalhar. Os parâmetros perguntados são:

Pergunta Descrição
Nome do projeto Identificador único do projeto, em caixa-baixa e separado por -
Versão Versão de acordo com o semver
Título Título da sua extensão para exibição na Rung Store
Description Descrição humana da proposta da sua extensão
Category Identificador da categoria da sua extensão. Padrão: miscellaneous
Licença Licença em que sua extensão é distribuída. Padrão MIT

Após confirmar, serão gerados package.json, index.js e README.md com as informações básicas de uma extensão.

Documentação da extensão

É possível usar o comando rung readme para gerar a documentação básica para uma extensão, incluindo informações sobre os parâmetros de entrada e dinâmicas (exemplo).

Banco de dados

Existem dois comandos no Rung CLI para trabalhar com banco de dados via linha de comando:

Comando Descrição
rung db clear Limpa toda a base de dados para a extensão ativa
rung db read Permite visualizar em formato Yaml os dados gravados no banco
_images/db-read.png

Extras

Extraindo HTML

Esse exemplo visa mostrar a possibilidade de extrair o HTML do site que quer obter o conteúdo e manipular o DOM fictício para poder obter os dados em específico. Iremos criar uma extensão que gera alertas contendo os nomes de todas as drag queens do programa de TV americando _RuPaul’s Drag Race_.

Estaremos usando o seguinte site como base: https://en.wikipedia.org/wiki/List_of_RuPaul%27s_Drag_Race_contestants

O conteúdo que queremos encontra-se em uma tabela no site:

_images/table.png

É necessário conhecer o básico de seletores do DOM. Se você sabe CSS, você provavelmente tem o conhecimento necessário para selecionar os elementos estáticos.

A tabela que queremos possui uma classe .sortable. Nós queremos pegar, do corpo da tabela cada linha e, de cada linha, o primeiro elemento. Então, podemos montar nosso seletor como .sortable tbody tr > td:nth-child(1). Se você abrir seu console e digitar document.querySelectorAll('.sortable tbody tr > td:nth-child(1)'), verá que recebemos uma lista de nodos HTML com as informações que queremos!

A requisição para obter HTML é igual à requisição para obter JSON. Podemos usar a Superagent para isso, e também vamos usar uma outra dependência. Adicione o jsdom ao projeto. Usaremos ele para criar o DOM virtual!

import Bluebird from 'bluebird';
import agent from 'superagent';
import promisifyAgent from 'superagent-promise';
import { JSDOM } from 'jsdom';

const request = promisifyAgent(agent, Bluebird);
const website = 'https://en.wikipedia.org/wiki/List_of_RuPaul%27s_Drag_Race_contestants';

Uma informação importante: quando buscamos uma lista de elementos do DOM, não nos é retornado um array do JavaScript, mas um NodeList. Existe uma maneira de contornar isso para trabalhar com os elementos, que é converter a lista de nodos para um array nativo. Podemos definir a seguinte função, baseado neste link:

function nodeListToArray(dom) {
    return Array.prototype.slice.call(dom, 0);
}

Agora, dentro da função principal assíncrona, precisamos extrair os dados. Uma informação importante é que alguns dos itens da lista que precisamos encontram-se no formato <a href="#">Queen</a> e outros no formato Queen.

function main(context, done) {
    // Obter todo o HTML do site em modo texto
    request.get(website).then(({ text }) => {
        // Virtualizar o DOM do texto
        const { window } = new JSDOM(text);
        // Converter os dados da tabela para uma lista e remover os links
        const queens = nodeListToArray(window.document.querySelectorAll('.sortable tbody tr > td:nth-child(1)'))
            .map(queen => {
                const link = queen.querySelector('a');
                return link === null ? queen.innerHTML : link.innerHTML;
            });

        // Agora, com `queens` contendo a lista que queremos, podemos gerar os alertas
        done({ alerts: queens });
    });
}

Basicamente, é possível usar os seletores também utilizados no CSS para extrair os elementos.

Resolução de problemas

Este documento visa listar alguns problemas comuns durante a criação de uma extensão e como eles podem ser resolvidos.

Ajuda ou parceiras?

Contate-nos em <developer@rung.com.br>