diff --git a/index.ts b/index.ts index 36cb03a..40c4157 100644 --- a/index.ts +++ b/index.ts @@ -148,6 +148,23 @@ class LiquidoPontoEntrada { initial: 1 }); + const perguntaLinguagemDeBackEnd = await prompts({ + type: 'select', + name: 'linguagemBackEnd', + message: 'Selecione a linguagem de programação', + choices: [ + { + title: 'Delégua', + value: 'delegua' + }, + { + title: 'Pituguês', + value: 'pitugues' + } + ], + initial: 1 + }); + const perguntaInicializarRepositorioGit = await prompts({ type: 'confirm', message: 'Deseja inicializar um repositório Git?', @@ -176,6 +193,8 @@ class LiquidoPontoEntrada { const gerenciadorDePacotes = perguntaQualGerenciadorDePacotesQuerUsar.gerenciadorDePacotes; + const linguagemSelecionada = perguntaLinguagemDeBackEnd.linguagemBackEnd; + await detectarGerenciadorDePacotes( gerenciadorDePacotes, diretorioCompleto @@ -183,6 +202,7 @@ class LiquidoPontoEntrada { await copiarArquivosDeExemploParaNovoProjeto( nomeProjeto, + linguagemSelecionada, perguntaTipoProjeto.tipoProjeto, diretorioCompleto ); diff --git a/infraestrutura/avaliador-sintatico-liquido.ts b/infraestrutura/avaliador-sintatico-liquido.ts new file mode 100644 index 0000000..d96780a --- /dev/null +++ b/infraestrutura/avaliador-sintatico-liquido.ts @@ -0,0 +1,86 @@ +import { AvaliadorSintaticoComImportacao } from '@designliquido/delegua-node'; +import { AvaliadorSintaticoPituguesComImportacao } from '@designliquido/delegua-node/avaliador-sintatico/dialetos/avaliador-sintatico-pitugues-com-importacao'; +import { Importador } from '@designliquido/delegua-node/importador'; +import tiposDeSimbolos from '@designliquido/delegua/tipos-de-simbolos/pitugues'; +import { Decorador } from '@designliquido/delegua'; + +export class AvaliadorSintaticoDeleguaLiquido extends AvaliadorSintaticoComImportacao { + constructor(importador: Importador) { + super(importador); + } +} + +export class AvaliadorSintaticoPituguesLiquido extends AvaliadorSintaticoPituguesComImportacao { + tiposDeFerramentasExternas: { + [nomeFerramenta: string]: { + [nomeTipo: string]: string; + }; + }; + + constructor(importador: Importador) { + super(importador); + } + + protected async resolverDecoradores(): Promise { + while (this.verificarTipoSimboloAtual(tiposDeSimbolos.ARROBA)) { + this.avancarEDevolverAnterior(); + + let nomeDecorador = ''; + let linha: number; + const atributos: { [key: string]: any } = {}; + + const primeiraParteNomeDecorador = this.consumir( + tiposDeSimbolos.IDENTIFICADOR, + 'Esperado nome de decorador após "@".' + ); + + linha = Number(primeiraParteNomeDecorador.linha); + nomeDecorador += primeiraParteNomeDecorador.lexema; + + while (this.verificarSeSimboloAtualEIgualA(tiposDeSimbolos.PONTO)) { + const parteNomeDecorador = this.consumir( + tiposDeSimbolos.IDENTIFICADOR, + 'Esperado nome de decorador após "."' + ); + + nomeDecorador += '.' + parteNomeDecorador.lexema; + } + + if ( + this.verificarSeSimboloAtualEIgualA(tiposDeSimbolos.PARENTESE_ESQUERDO) + ) { + if ( + !this.verificarTipoSimboloAtual(tiposDeSimbolos.PARENTESE_DIREITO) + ) { + let indexArgumento = 0; + + do { + const valorExpressao = await this.expressao(); + + atributos[indexArgumento] = valorExpressao; + + if (indexArgumento === 0) { + atributos['caminho'] = valorExpressao; + } + + indexArgumento++; + } while (this.verificarSeSimboloAtualEIgualA(tiposDeSimbolos.VIRGULA)); + } + + this.consumir( + tiposDeSimbolos.PARENTESE_DIREITO, + 'Esperado ")" após argumentos do decorador.' + ); + } + + this.pilhaDecoradores.push( + new Decorador( + this.hashArquivo, + linha, + nomeDecorador, + atributos + ) + ); + } + } +} \ No newline at end of file diff --git a/infraestrutura/centro-configuracoes/configuracao-liquido.ts b/infraestrutura/centro-configuracoes/configuracao-liquido.ts index 6ab8f20..a436e04 100644 --- a/infraestrutura/centro-configuracoes/configuracao-liquido.ts +++ b/infraestrutura/centro-configuracoes/configuracao-liquido.ts @@ -5,7 +5,8 @@ import { ConfiguracaoDados } from "./configuracao-dados"; import { ConfiguracaoRoteador } from "./configuracao-roteador"; export class ConfiguracaoLiquido extends ConfiguracaoComum { - arquetipo?: 'rest' | 'mvc'; + arquetipo: 'rest' | 'mvc'; + linguagem: 'delegua' | 'pitugues'; aplicacao: ConfiguracaoAplicacao; autenticacao: ConfiguracaoAutenticacao; dados: ConfiguracaoDados; diff --git a/infraestrutura/interpretador-liquido.ts b/infraestrutura/interpretador-liquido.ts index 411a26f..9b158e5 100644 --- a/infraestrutura/interpretador-liquido.ts +++ b/infraestrutura/interpretador-liquido.ts @@ -1,9 +1,15 @@ import { InterpretadorComImportacao } from "@designliquido/delegua-node/interpretador"; +import { InterpretadorPituguesComImportacao } from "@designliquido/delegua-node/interpretador/dialetos/interpretador-pitugues-com-importacao"; + /** * A única função deste interpretador é resolver bugs que precisam ser repassados * para o núcleo de Delégua. */ export class InterpretadorLiquido extends InterpretadorComImportacao { - + +} + +export class InterpretadorLiquidoPitugues extends InterpretadorPituguesComImportacao { + } \ No newline at end of file diff --git a/interface-linha-comando/exemplos/api-rest/LEIAME.md b/interface-linha-comando/exemplos/delegua/api-rest/LEIAME.md similarity index 100% rename from interface-linha-comando/exemplos/api-rest/LEIAME.md rename to interface-linha-comando/exemplos/delegua/api-rest/LEIAME.md diff --git a/interface-linha-comando/exemplos/api-rest/configuracao.delprops b/interface-linha-comando/exemplos/delegua/api-rest/configuracao.delprops similarity index 96% rename from interface-linha-comando/exemplos/api-rest/configuracao.delprops rename to interface-linha-comando/exemplos/delegua/api-rest/configuracao.delprops index 04caa8f..1ce3286 100644 --- a/interface-linha-comando/exemplos/api-rest/configuracao.delprops +++ b/interface-linha-comando/exemplos/delegua/api-rest/configuracao.delprops @@ -1,5 +1,6 @@ // Configuração da aplicação em si. liquido.arquetipo = 'rest' +liquido.linguagem = 'delégua' liquido.aplicacao.nome = 'Minha aplicação' liquido.aplicacao.versao = '0.0.0' liquido.aplicacao.descricao = 'A minha aplicação é uma API REST.' diff --git a/interface-linha-comando/exemplos/api-rest/rotas/LEIAME.md b/interface-linha-comando/exemplos/delegua/api-rest/rotas/LEIAME.md similarity index 100% rename from interface-linha-comando/exemplos/api-rest/rotas/LEIAME.md rename to interface-linha-comando/exemplos/delegua/api-rest/rotas/LEIAME.md diff --git a/interface-linha-comando/exemplos/api-rest/rotas/inicial.delegua b/interface-linha-comando/exemplos/delegua/api-rest/rotas/inicial.delegua similarity index 100% rename from interface-linha-comando/exemplos/api-rest/rotas/inicial.delegua rename to interface-linha-comando/exemplos/delegua/api-rest/rotas/inicial.delegua diff --git a/interface-linha-comando/exemplos/mvc/LEIAME.md b/interface-linha-comando/exemplos/delegua/mvc/LEIAME.md similarity index 100% rename from interface-linha-comando/exemplos/mvc/LEIAME.md rename to interface-linha-comando/exemplos/delegua/mvc/LEIAME.md diff --git a/interface-linha-comando/exemplos/mvc/configuracao.delprops b/interface-linha-comando/exemplos/delegua/mvc/configuracao.delprops similarity index 96% rename from interface-linha-comando/exemplos/mvc/configuracao.delprops rename to interface-linha-comando/exemplos/delegua/mvc/configuracao.delprops index a8cd06c..b0b0513 100644 --- a/interface-linha-comando/exemplos/mvc/configuracao.delprops +++ b/interface-linha-comando/exemplos/delegua/mvc/configuracao.delprops @@ -1,5 +1,6 @@ // Configuração da aplicação em si. liquido.arquetipo = 'mvc' +liquido.linguagem = 'delégua' liquido.aplicacao.nome = 'Minha aplicação' liquido.aplicacao.versao = '0.0.0' liquido.aplicacao.descricao = 'A minha aplicação é um MVC.' diff --git a/interface-linha-comando/exemplos/mvc/estilos/LEIAME.md b/interface-linha-comando/exemplos/delegua/mvc/estilos/LEIAME.md similarity index 100% rename from interface-linha-comando/exemplos/mvc/estilos/LEIAME.md rename to interface-linha-comando/exemplos/delegua/mvc/estilos/LEIAME.md diff --git a/interface-linha-comando/exemplos/mvc/modelos/LEIAME.md b/interface-linha-comando/exemplos/delegua/mvc/modelos/LEIAME.md similarity index 100% rename from interface-linha-comando/exemplos/mvc/modelos/LEIAME.md rename to interface-linha-comando/exemplos/delegua/mvc/modelos/LEIAME.md diff --git a/interface-linha-comando/exemplos/mvc/parciais/LEIAME.md b/interface-linha-comando/exemplos/delegua/mvc/parciais/LEIAME.md similarity index 100% rename from interface-linha-comando/exemplos/mvc/parciais/LEIAME.md rename to interface-linha-comando/exemplos/delegua/mvc/parciais/LEIAME.md diff --git a/interface-linha-comando/exemplos/mvc/publico/LEIAME.md b/interface-linha-comando/exemplos/delegua/mvc/publico/LEIAME.md similarity index 100% rename from interface-linha-comando/exemplos/mvc/publico/LEIAME.md rename to interface-linha-comando/exemplos/delegua/mvc/publico/LEIAME.md diff --git a/interface-linha-comando/exemplos/mvc/rotas/LEIAME.md b/interface-linha-comando/exemplos/delegua/mvc/rotas/LEIAME.md similarity index 100% rename from interface-linha-comando/exemplos/mvc/rotas/LEIAME.md rename to interface-linha-comando/exemplos/delegua/mvc/rotas/LEIAME.md diff --git a/interface-linha-comando/exemplos/mvc/visoes/LEIAME.md b/interface-linha-comando/exemplos/delegua/mvc/visoes/LEIAME.md similarity index 100% rename from interface-linha-comando/exemplos/mvc/visoes/LEIAME.md rename to interface-linha-comando/exemplos/delegua/mvc/visoes/LEIAME.md diff --git a/interface-linha-comando/exemplos/pitugues/api-rest/LEIAME.md b/interface-linha-comando/exemplos/pitugues/api-rest/LEIAME.md new file mode 100644 index 0000000..9d84402 --- /dev/null +++ b/interface-linha-comando/exemplos/pitugues/api-rest/LEIAME.md @@ -0,0 +1,180 @@ +# API REST + +Este padrão de projeto implementa o que chamamos de API (_Application Programming Interface_, ou Interface de Programação de Aplicação) REST (_Representational State Transfer_, ou Transferência de Estado Representacional). + +## Não li e nem lerei + +Se você não tem paciência para ler o documento inteiro, colocamos em `rotas/inicial.pitu` um exemplo funcional de rotas trabalhando com JSON. Há um outro documento `LEIAME.md` dentro do diretório `rotas` que pode ajudar. + +Se tem, boa leitura. + +## Siglas API e REST + +É importante entender o que cada uma dessas siglas significa separadamente. + +**APIs** normalmente expõem componentes que são usados dentro de uma aplicação, como métodos, propriedades e classes, e são tipicamente distribuídas por pessoas ou empresas para uso por terceiros (ou consumidores). Por definição, uma interface não expõe como esses métodos, propriedades e classes são implementados. A maioria das interfaces possui uma documentação, e esta documentação orienta os consumidores sobre como utilizar cada componente da interface. + +**REST** é um dos vários protocolos da Internet. Em REST, uma aplicação expõe uma série de recursos. Cada recurso é acessível através de um conjunto de endereços e métodos. + +Cada endereço segue uma convenção que chamamos de URL (_Universal Resource Locator_, ou Localizador Universal de Recurso). _Sites_, ou sítios da internet, usualmente são acessíveis por um endereço que indica qual protocolo de transferência deve ser utilizando (HTTP e FTP são os mais populares, mas há muitos outros), seguido de `://` (dois-pontos e duas barras), um localizador DNS (_Domain Name Server_, ou Servidor de Nomes de Domínio) e um caminho. Por exemplo, `http://designliquido.com.br`. O protocolo é HTTP, e o endereço DNS da empresa que construiu Líquido é `designliquido.com.br` (`.com` quer dizer que é um sítio comercial, e `.br` quer dizer que fica no Brasil). + +Ao acessar o sítio da Design Líquido no seu navegador de internet, o navegador assume um método (ou verbo) padrão. Por padrão, toda e qualquer requisição cujo método não esteja especificado usa o método `GET` (obter). Este método indica que queremos ler o conteúdo correspondente ao endereço. O servidor da Design Líquido irá receber esta requisição, montar uma página em HTML e devolver. + +Todo sítio da internet é, por definição, uma API REST, que normalmente nos devolve HTML como retorno, mas nada nos impediria de retornar qualquer outra coisa serializável. HTML, XML e JSON são exemplos de formatos serializáveis. + +APIs REST se tornaram muito populares com a criação de _smartphones_. Antes dos _smartphones_, um outro padrão de APIs dominava a internet, chamado SOAP. SOAP se parece muito com REST, mas suas APIs trabalhavam apenas com um método (`POST`) e suas respostas são bastante longas e verbosas, o que oneravam sobremaneira e desnecessariamente o processamento em um _smartphone_ da época, bem mais lento e limitado que o _smartphone_ mais barato hoje. Surgiu a necessidade de não apenas simplificar as APIs, como também deixar as respostas menores. + +# Serialização + +Serialização é um processo de estruturação de dados. Essa estruturação pode ser legível a seres humanos (por exemplo, JSON, XML, YAML) ou não (Protobuf, binário, etc.), sendo os formatos menos legíveis os mais otimizados para uso por máquinas. A serialização é feita por serializadores, e o processo de desestruturação desses dados é chamado de desserialização. Serializadores e desserializadores seguem a especificação do formato que implementam. + +Existem centenas de formatos de serialização, que servem a diferentes propósitos. JSON e XML, por exemplo, são muito bons para estruturar dados aninhados, em uma enorme quantidade de detalhes. + +A melhor forma de explicar serialização é por exemplos. Vamos supor que queremos construir uma API REST para um blog. O primeiro recurso que queremos implementar é o de leitura de artigos, e vamos supor que temos três artigos já escritos: + +- Primeiro artigo + - Título: O que é REST? + - Texto: REST significa "Transferência de Estado Representacional". + - Autor: Leonel +- Segundo artigo + - Título: O que é API? + - Texto: API significa "Interface de Programação de Aplicação". + - Autor: Leonel +- Terceiro artigo + - Título: Bem-vindos! + - Texto: Este é meu blog. + - Autor: Leonel + +Se queremos serializar o primeiro artigo em JSON, podemos fazê-lo da seguinte forma: + +```json +{ + "titulo": "O que é REST?", + "texto": "REST significa 'Transferência de Estado Representacional'.", + "autor": "Leonel" +} +``` + +Chaves, aspas duplas, dois-pontos e vírgulas são delimitadores. São usados para definir onde uma porção de informação começa e/ou termina, dependendo do caso. A vírgula, por exemplo, é usada para separar porções de dados. Dois-pontos é usado para separar uma chave de um valor. `"titulo"`, `"texto"` e `"autor"` são, portanto, chaves. O que vem após o sinal de dois-pontos é o valor correspondente a cada chave. Em JSON, todas as chaves são porções de dados delimitadas por aspas duplas, mas valores podem ser delimitados por aspas duplas, chaves, e até mesmo colchetes. Em JSON, o nome "chave" não é à toa: Não é possível um objeto ter duas chaves idênticas. + +Colchetes são usados para definir listas de valores. Por exemplo, se queremos definir a lista de artigos numa representação JSON, podemos fazer algo assim: + +```json +[ + { + "titulo": "O que é REST?", + "texto": "REST significa 'Transferência de Estado Representacional'.", + "autor": "Leonel" + }, + { + "titulo": "O que é API?", + "texto": "API significa 'Interface de Programação de Aplicação'.", + "autor": "Leonel" + }, + { + "titulo": "Bem-vindos!", + "texto": "Este é meu blog.", + "autor": "Leonel" + } +] +``` + +Ou seja, temos três artigos, sendo cada artigo delimitado por chaves e separado por vírgulas. Cada artigo possui três pares chave-valor, separados por vírgulas. Os três artigos aparecem entre colchetes, indicando uma lista de artigos. + +JSON é exatamente poderoso pela recombinação de diversos elementos de dados. Por exemplo, poderíamos fazer nosso recurso de listagem de artigos devolver não apenas uma lista de artigos, mas informações adicionais sobre eles. Por exemplo: + +```json +{ + "pagina": "Meu blog", + "totalArtigos": 3, + "artigos": [ + { + "titulo": "O que é REST?", + "texto": "REST significa 'Transferência de Estado Representacional'.", + "autor": "Leonel" + }, + { + "titulo": "O que é API?", + "texto": "API significa 'Interface de Programação de Aplicação'.", + "autor": "Leonel" + }, + { + "titulo": "Bem-vindos!", + "texto": "Este é meu blog.", + "autor": "Leonel" + } + ] +} +``` + +Ou seja, aninhamos os artigos como valor de uma chave `"artigos"`, dentro de um outro objeto, com outras chaves, que contém valores de diferentes tipos. Números, por exemplo, não requerem delimitadores. + +## Serialização para JSON em Líquido + +O método `.json()` do objeto `resposta` serializa um dicionário em Pituguês para a representação JSON. Dicionários em Pituguês são muito parecidos com objetos JSON, com algumas diferenças: + +- Dicionários em Pituguês permitem chaves como números. Objetos JSON permitem apenas chaves delimitadas por aspas duplas. + +Para usar, basta passar qualquer dicionário, seja literal ou variável, como argumento de `resposta.json()`: + +```js +@liquido.rotaGet("/") +funcao minha_rota(requisicao, resposta): + resposta.json([{ + "id": 1, + "titulo": "teste 1", + "descricao": "descricao 1" + }]) +``` + +## Auto-documentação + +Para projetos REST, Liquido possui capacidades de auto-documentação, ou seja, gerar uma série de documentos que explicam como a API REST que você está escrevendo irá funcionar. + +Uma boa parte dos elementos são depreendidos pelo método de rota usado, o tipo de retorno usado para a resposta, e assim por diante. Outros podem ser adicionados por decoradores. + +Do exemplo anterior: + +```js +@liquido.rotaGet("/") +funcao minha_rota(requisicao, resposta): + resposta.json([{ + "id": 1, + "titulo": "teste 1", + "descricao": "descricao 1" + }]) +``` + +- Sabemos a rota pela posição do arquivo controlador na estrutura de diretórios; +- Sabemos que a rota responde pelo método `GET`; +- Sabemos que o tipo da resposta é feito por `resposta.json()`, portanto, um conteúdo JSON. + +Nosso modelo de auto-documentação é o [OpenAPI 3.1.0](https://swagger.io/specification/). A geração dessa documentação pode ser feita de duas maneiras: + +- Na inicialização do servidor; +- Por linha de comando. + +### Decoradores para auto-documentação + +Os decoradores suportados atualmente estão como no exemplo abaixo: + +```js +@rest.documentacao( + sumario = "Um exemplo de rota GET.", + descricao = "Uma descrição mais detalhada sobre como a rota GET funciona.", + idOperacao = "lerArtigos", + etiquetas = ["artigos"] +) +@rest.resposta( + codigo = 200, + descricao = "Devolvido com sucesso", + formatos = ["application/json", "application/xml"] +) +@liquido.rotaGet("/") +funcao minha_rota(requisicao, resposta): + resposta.json([{ + "id": 1, + "titulo": "teste 1", + "descricao": "descricao 1" + }]) +``` \ No newline at end of file diff --git a/interface-linha-comando/exemplos/pitugues/api-rest/configuracao.delprops b/interface-linha-comando/exemplos/pitugues/api-rest/configuracao.delprops new file mode 100644 index 0000000..157f668 --- /dev/null +++ b/interface-linha-comando/exemplos/pitugues/api-rest/configuracao.delprops @@ -0,0 +1,24 @@ +// Configuração da aplicação em si. +liquido.arquetipo = 'rest' +liquido.linguagem = 'pituguês' +liquido.aplicacao.nome = 'Minha aplicação' +liquido.aplicacao.versao = '0.0.0' +liquido.aplicacao.descricao = 'A minha aplicação é uma API REST.' +liquido.aplicacao.licenca.nome = 'MIT' +liquido.aplicacao.licenca.url = 'https://github.com/DesignLiquido/liquido/LICENSE' + +// Configuração de arquivos estáticos +liquido.roteador.diretorioEstatico = 'publico' + +// Configuração do roteador. +liquido.roteador.cors = verdadeiro +liquido.roteador.bodyParser = verdadeiro +liquido.roteador.morgan = verdadeiro +liquido.roteador.cookieParser = verdadeiro +liquido.roteador.passport = falso +liquido.roteador.json = verdadeiro +liquido.roteador.helmet = verdadeiro + +// Configuração de bases de dados +liquido.dados.lincones.tecnologia = 'sqlite' +liquido.dados.lincones.caminho = ':memory:' \ No newline at end of file diff --git a/interface-linha-comando/exemplos/pitugues/api-rest/rotas/LEIAME.md b/interface-linha-comando/exemplos/pitugues/api-rest/rotas/LEIAME.md new file mode 100644 index 0000000..9c52f17 --- /dev/null +++ b/interface-linha-comando/exemplos/pitugues/api-rest/rotas/LEIAME.md @@ -0,0 +1,30 @@ +# Diretório `rotas` + +Neste diretório temos uma série de controladores que implementam uma ou mais rotas. Uma rota é um endereço da internet, como por exemplo `http://localhost:3000`, `http://localhost:3000/blog`, `http://localhost:3000/blog/meu-primeiro-artigo`, e assim por diante. Rotas normalmente funcionam com métodos, como GET, POST, PUT e DELETE, e implementadas em Pituguês. + +Rotas seguem uma convenção de diretórios. Por exemplo, se queremos implementar uma rota que responda em `http://localhost:3000` (ou seja, a rota raiz), devemos criar neste diretório um arquivo com o nome `inicial.pitu`. Um exemplo de arquivo inicial contém o seguinte: + +```js +@liquido.rotaGet("/") +funcao rota_get(requisicao, resposta): + resposta.enviar("Olá mundo").status(200) +``` + +No entanto, o normal de uma rota REST é servir JSON, XML, ou outros formatos que chamamos de serializáveis. No caso de JSON, usamos o seguinte: + +```js +@liquido.rotaGet("/") +funcao rota_get(requisicao, resposta): + resposta.json({ + "id": 1, + "titulo": "Meu primeiro artigo", + "descricao": "Este é meu primeiro artigo." + }).status(200) +``` + +Se executarmos Liquido em modo servidor e tentarmos acessar `http://localhost:3000` no nosso navegador, se tudo foi feito da maneira certa, teremos uma página com o texto "Olá mundo". + +Seguindo os exemplos dados, se quisermos implementar `http://localhost:3000/artigos`, temos duas boas opções: + +- Criar dentro de `rotas` um diretório `artigos`, e dentro desse diretório `artigos` um arquivo `inicial.pitu`, com pelo menos uma configuração de rota dentro; +- Criar dentro de `rotas` um arquivo `artigos.pitu`, com pelo menos uma configuração de rota dentro. \ No newline at end of file diff --git a/interface-linha-comando/exemplos/pitugues/api-rest/rotas/inicial.pitu b/interface-linha-comando/exemplos/pitugues/api-rest/rotas/inicial.pitu new file mode 100644 index 0000000..4689a42 --- /dev/null +++ b/interface-linha-comando/exemplos/pitugues/api-rest/rotas/inicial.pitu @@ -0,0 +1,22 @@ +@rest.documentacao( + sumario = "Um exemplo de rota GET.", + descricao = "Uma descrição mais detalhada sobre como a rota GET funciona.", + idOperacao = "lerArtigos", + etiquetas = ["artigos"] +) +@rest.resposta( + codigo = 200, + descricao = "Devolvido com sucesso", + formatos = ["application/json", "application/xml"] +) +@liquido.rotaGet("/") +funcao rota_principal(requisicao, resposta): + resposta.json([{ + "id": 1, + "titulo": "teste 1", + "descricao": "descricao 1" + }]) + +@liquido.rotaPost("/artigos") +funcao minha_rota_post(requisicao, resposta): + resposta.redirecionar("/artigos") \ No newline at end of file diff --git a/interface-linha-comando/exemplos/pitugues/mvc/LEIAME.md b/interface-linha-comando/exemplos/pitugues/mvc/LEIAME.md new file mode 100644 index 0000000..bba79b3 --- /dev/null +++ b/interface-linha-comando/exemplos/pitugues/mvc/LEIAME.md @@ -0,0 +1,30 @@ +# MVC + +Este padrão de projeto implementa o que chamamos de MVC (Model-View-Controller, ou Modelo-Visão-Controlador), uma arquitetura em 3 camadas para _sites_, ou sítios, da internet. + +## Não li e nem lerei + +Se você não tem paciência para ler o documento inteiro, colocamos em cada diretório o mínimo que uma aplicação MVC em Líquido precisa ter. Fique à vontade para explorar cada diretório, que também possui um arquivo `LEIAME.md` correspondente. + +Se tem, boa leitura. + +## Arquiteturas em 3 Camadas e o MVC + +Aplicações que consomem dados de fontes de dados independentes normalmente são implementadas em pelo menos duas camadas: + +- Cliente: uma camada com uma interface, em que o usuário pode realizar operações com dados; +- Servidor: uma camada que possui os dados, retornando-os de acordo com a operação pedida pelo cliente. + +Se trabalhamos com mais de um servidor, seja pela natureza dos dados, seja por como esses servidores se dispoõem materialmente, é comum separarmos o servidor em duas camadas independentes, obtendo, assim, três camadas: + +- Cliente (ou Apresentação) +- Negócio +- Dados + +Essa arquitetura ainda é muito utilizada até hoje, não importando a idade da aplicação. A camada de dados sabe como acessar os dados; a camada de negócio pede diferentes dados para diferentes pontos de entrada na camada de dados; esses dados são organizados e enviados para a camada de cliente (ou camada de apresentação), exibindo os dados ao usuário. + +O MVC é uma especialização dessa arquitetura de três camadas, repensadas da seguinte forma: + +- A camada de dados, ao invés de descrever detalhadamente como obter e retornar os dados, descreve entidades e seus relacionamentos em alto nível. Cada entidade dessa camada é chamada de Modelo, e o conjunto de modelos também é um Modelo; +- A camada de negócio trabalha com um mapeador objeto-relacional, ou um repositório de dados, dependendo das tecnologias dos bancos de dados. Mapeadores objeto-relacional e repositórios sabem ler as definições do Modelo, não apenas lendo e alterando dados, mas também controlando o esquema de dados. O esquema de dados é a materialização dos descritivos de entidades e seus relacionamentos. Em outras palavras, esses dois componentes sabem criar tabelas e coleções, bem como alterá-las e até removê-las. Cada componente desta camada é chamado de Controlador, que é o mesmo nome da camada: Controlador; +- A camada cliente, ou de apresentação, passa a ter diferentes formas de apresentar os dados, obedecendo a critérios como o tipo do requisitor, quais tipos de dados o requisitor aceita, e capacidades adicionais de apresentação dos dados pelo requisitor. Por exemplo, nossa aplicação pode responder a determinadas requisições como HTML, e outras como JSON. Cada documento que descreve essa apresentação de dados é chamado de Visão, que é o mesmo nome do conjunto de visões como camada: Visão. diff --git a/interface-linha-comando/exemplos/pitugues/mvc/configuracao.delprops b/interface-linha-comando/exemplos/pitugues/mvc/configuracao.delprops new file mode 100644 index 0000000..ff6164a --- /dev/null +++ b/interface-linha-comando/exemplos/pitugues/mvc/configuracao.delprops @@ -0,0 +1,24 @@ +// Configuração da aplicação em si. +liquido.arquetipo = 'mvc' +liquido.linguagem = 'pituguês' +liquido.aplicacao.nome = 'Minha aplicação' +liquido.aplicacao.versao = '0.0.0' +liquido.aplicacao.descricao = 'A minha aplicação é um MVC.' +liquido.aplicacao.licenca.nome = 'MIT' +liquido.aplicacao.licenca.url = 'https://github.com/DesignLiquido/liquido/LICENSE' + +// Configuração de arquivos estáticos +liquido.roteador.diretorioEstatico = 'publico' + +// Configuração do roteador. +liquido.roteador.cors = verdadeiro +liquido.roteador.bodyParser = verdadeiro +liquido.roteador.morgan = verdadeiro +liquido.roteador.cookieParser = verdadeiro +liquido.roteador.passport = falso +liquido.roteador.json = verdadeiro +liquido.roteador.helmet = verdadeiro + +// Configuração de bases de dados +liquido.dados.lincones.tecnologia = 'sqlite' +liquido.dados.lincones.caminho = ':memory:' \ No newline at end of file diff --git a/interface-linha-comando/exemplos/pitugues/mvc/estilos/LEIAME.md b/interface-linha-comando/exemplos/pitugues/mvc/estilos/LEIAME.md new file mode 100644 index 0000000..3547be8 --- /dev/null +++ b/interface-linha-comando/exemplos/pitugues/mvc/estilos/LEIAME.md @@ -0,0 +1,3 @@ +# Diretório `estilos` + +Neste diretório, colocamos todos os arquivos com a extensão `.foles`. [FolEs](https://github.com/DesignLiquido/foles) é uma linguagem de estilização 100% em português criada pela Design Líquido para traduzir de e para CSS. CSS é uma linguagem de folhas de estilo em inglês. \ No newline at end of file diff --git a/interface-linha-comando/exemplos/pitugues/mvc/modelos/LEIAME.md b/interface-linha-comando/exemplos/pitugues/mvc/modelos/LEIAME.md new file mode 100644 index 0000000..d012bbb --- /dev/null +++ b/interface-linha-comando/exemplos/pitugues/mvc/modelos/LEIAME.md @@ -0,0 +1,5 @@ +# Diretório `modelos` + +Este diretório contém todos os modelos de dados usados pela aplicação. Modelos normalmente definem registros de dados em tecnologias de persistência, como caches e bancos de dados. + +Modelos devem ser classes em Delégua que não precisam ter métodos: apenas propriedades. \ No newline at end of file diff --git a/interface-linha-comando/exemplos/pitugues/mvc/parciais/LEIAME.md b/interface-linha-comando/exemplos/pitugues/mvc/parciais/LEIAME.md new file mode 100644 index 0000000..98687f8 --- /dev/null +++ b/interface-linha-comando/exemplos/pitugues/mvc/parciais/LEIAME.md @@ -0,0 +1,3 @@ +# Diretório `parciais` + +Este diretório contém fragmentos de arquivos LMHT, que podem ser reutilizados entre múltiplas apresentações. [LMHT](https://github.com/DesignLiquido/LMHT) é uma linguagem de estruturação de documentos 100% em português, traduzindo de e para HTML, que é sua contraparte em inglês. \ No newline at end of file diff --git a/interface-linha-comando/exemplos/pitugues/mvc/publico/LEIAME.md b/interface-linha-comando/exemplos/pitugues/mvc/publico/LEIAME.md new file mode 100644 index 0000000..5907e3c --- /dev/null +++ b/interface-linha-comando/exemplos/pitugues/mvc/publico/LEIAME.md @@ -0,0 +1,3 @@ +# Diretório `publico` + +Este diretório contém o que chamamos de recursos estáticos, como imagens, folhas de estilo já processadas e outros arquivos que possam ser de interesse da aplicação. \ No newline at end of file diff --git a/interface-linha-comando/exemplos/pitugues/mvc/rotas/LEIAME.md b/interface-linha-comando/exemplos/pitugues/mvc/rotas/LEIAME.md new file mode 100644 index 0000000..faabf56 --- /dev/null +++ b/interface-linha-comando/exemplos/pitugues/mvc/rotas/LEIAME.md @@ -0,0 +1,55 @@ +# Diretório `rotas` + +Neste diretório temos uma série de controladores que implementam uma ou mais rotas. Uma rota é um endereço da internet, como por exemplo `http://localhost:3000`, `http://localhost:3000/blog`, `http://localhost:3000/blog/meu-primeiro-artigo`, e assim por diante. Rotas normalmente funcionam com métodos, como GET, POST, PUT e DELETE, e implementadas em Pituguês. + +Rotas seguem uma convenção de diretórios. Por exemplo, se queremos implementar uma rota que responda em `http://localhost:3000` (ou seja, a rota raiz), devemos criar neste diretório um arquivo com o nome `inicial.pitu`. Um exemplo de arquivo inicial contém o seguinte: + +```js +@liquido.rotaGet("/") +funcao minha_rota(requisicao, resposta): + resposta.enviar("Olá mundo").status(200) +``` + +Se executarmos Liquido em modo servidor e tentarmos acessar `http://localhost:3000` no nosso navegador, se tudo foi feito da maneira certa, teremos uma página com o texto "Olá mundo". + +Seguindo os exemplos dados, se quisermos implementar `http://localhost:3000/blog`, temos duas boas opções: + +- Criar dentro de `rotas` um diretório `blog`, e dentro desse diretório blog um arquivo `inicial.pitu`, com pelo menos uma configuração de rota dentro; +- Criar dentro de `rotas` um arquivo `blog.pitu`, com pelo menos uma configuração de rota dentro. + +## Parametrização de rotas + +Rotas podem trabalhar com múltiplos parâmetros, seja como parte do caminho, seja como parâmetro de pesquisa. A diferença entre parâmetros de caminho e parâmetros de pesquisa é a localização deles no endereço: + +- Parâmetros de caminho ficam antes de um ponto de interrogação de um endereço; +- Parâmetros de pesquisa ficam após este ponto de interrogação do endereço. + +Por exemplo, na rota `https://localhost:3000/blog/categorias/1/pesquisar?titulo=Programação`, temos um parâmetro de caminho (o número `1` entre `/categorias/` e `/pesquisar/`) e um parâmetro de pesquisa (no caso, nome `titulo` e valor `Programação`). + +Em Líquido, a convenção de diretórios de rotas pode determinar a localização de parâmetros de caminho. Para termos uma rota que responde ao endereço de exemplo, precisamos ter: + +- Dentro do diretório `rotas`, um diretório `blog`; +- Dentro do diretório `blog`, um diretório `categorias`; +- Dentro do diretório `categorias`, um diretório cujo nome começa e termina em colchetes. Por exemplo, `[id]`. Neste caso, ao acessar o endereço, Líquido automaticamente cria dentro da função da rota um parâmetro de caminho com o nome `id` e o valor deverá ser `1`; +- Dentro do diretório `[id]`, ou um arquivo `pesquisar.pitu`, ou um diretório pesquisar com um arquivo `inicial.pitu`. + +Ou seja: + +``` +- rotas + - blog + - categorias + - [id] + - pesquisar.pitu +``` + +Ou: + +``` +- rotas + - blog + - categorias + - [id] + - pesquisar + - inicial.pitu +``` \ No newline at end of file diff --git a/interface-linha-comando/exemplos/pitugues/mvc/visoes/LEIAME.md b/interface-linha-comando/exemplos/pitugues/mvc/visoes/LEIAME.md new file mode 100644 index 0000000..64ed007 --- /dev/null +++ b/interface-linha-comando/exemplos/pitugues/mvc/visoes/LEIAME.md @@ -0,0 +1,3 @@ +# Diretório `visoes` + +Este diretório contém as visões do projeto. Cada visão normalmente é um arquivo em LMHT, mas outros formatos de arquivos podem ser suportados no futuro. [LMHT](https://github.com/DesignLiquido/LMHT) é uma linguagem de estruturação de documentos 100% em português, traduzindo de e para HTML, que é sua contraparte em inglês. \ No newline at end of file diff --git a/interface-linha-comando/novo/index.ts b/interface-linha-comando/novo/index.ts index 0dfb7f3..1e0f22a 100644 --- a/interface-linha-comando/novo/index.ts +++ b/interface-linha-comando/novo/index.ts @@ -18,13 +18,14 @@ export function criarDiretorioAplicacao(nomeAplicacao: string): string { export async function copiarArquivosDeExemploParaNovoProjeto( nomeProjeto: string, tipoDeProjeto: string, + linguagemDeBackEnd: string, diretorioProjeto: string ) { const diretorioExemplos = caminho.join( - __dirname, '../exemplos/' + tipoDeProjeto + __dirname, `../exemplos/${linguagemDeBackEnd}/` + tipoDeProjeto ); const formatoGlob = - (diretorioExemplos + '/**/*.{delprops,foles,lmht,md}') + (diretorioExemplos + '/**/*.{delegua,pitu,delprops,foles,lmht,md}') .replace(/\\/gi, '/'); const caminhosArquivos = await glob([formatoGlob], { diff --git a/interfaces/interface-liquido.ts b/interfaces/interface-liquido.ts index 53efa2c..1568244 100644 --- a/interfaces/interface-liquido.ts +++ b/interfaces/interface-liquido.ts @@ -16,8 +16,8 @@ export interface LiquidoInterface { diretorioEstatico: string; iniciar(): Promise; - descobrirRotas(diretorio: string): void; - resolverCaminhoRota(caminhoArquivo: string): string; + descobrirRotas(diretorio: string, linguagem: string): void; + resolverCaminhoRota(caminhoArquivo: string, linguagem: string): string; importarArquivosRotas(): void; importarArquivoConfiguracao(): void; resolverArquivoConfiguracao(caminhoTotal?: string): RetornoConfiguracaoInterface; diff --git a/liquido.ts b/liquido.ts index a732c27..3284dd5 100644 --- a/liquido.ts +++ b/liquido.ts @@ -1,13 +1,13 @@ import * as sistemaDeArquivos from 'fs'; import * as caminho from 'path'; -import { AvaliadorSintaticoComImportacao } from '@designliquido/delegua-node/avaliador-sintatico/avaliador-sintatico-com-importacao'; +import { AvaliadorSintaticoDeleguaLiquido, AvaliadorSintaticoPituguesLiquido } from './infraestrutura/avaliador-sintatico-liquido'; import { AcessoMetodo, Chamada, FuncaoConstruto, Variavel } from '@designliquido/delegua/construtos'; -import { Expressao, FuncaoDeclaracao } from '@designliquido/delegua/declaracoes'; +import { Expressao, FuncaoDeclaracao } from '@designliquido/delegua/declaracoes' import { DeleguaFuncao, ObjetoDeleguaClasse } from '@designliquido/delegua/interpretador/estruturas'; import { ErroInterpretadorInterface, InterpretadorInterface, ResultadoParcialInterpretadorInterface, RetornoInterpretadorInterface, SimboloInterface, VariavelInterface } from '@designliquido/delegua/interfaces'; import { InformacaoElementoSintatico } from '@designliquido/delegua/informacao-elemento-sintatico'; -import { Lexador, Simbolo } from '@designliquido/delegua/lexador'; +import { Lexador, LexadorPitugues, Simbolo } from '@designliquido/delegua/lexador'; import { Importador } from '@designliquido/delegua-node/importador'; @@ -22,16 +22,17 @@ import { CentroConfiguracoes } from './infraestrutura/centro-configuracoes'; import { AspectoConfiguracaoInterface } from './infraestrutura/centro-configuracoes/aspecto-configuracao-interface'; import { AutoDocumentador } from './infraestrutura/auto-documentacao/auto-documentador'; import { Requisicao } from './infraestrutura/requisicao'; -import { InterpretadorLiquido } from './infraestrutura/interpretador-liquido'; +import { InterpretadorLiquido, InterpretadorLiquidoPitugues } from './infraestrutura/interpretador-liquido'; import { RetornoQuebra } from '@designliquido/delegua/quebras'; import { listaDeErros } from './erros'; +import { Declaracao } from '@designliquido/foles/declaracoes'; /** * O núcleo do framework. */ export class Liquido implements LiquidoInterface { importador: Importador; - avaliadorSintatico: AvaliadorSintaticoComImportacao; + avaliadorSintatico: AvaliadorSintaticoDeleguaLiquido | AvaliadorSintaticoPituguesLiquido; interpretador: InterpretadorInterface; roteador: Roteador; formatadorLmht: FormatadorLmht; @@ -41,7 +42,9 @@ export class Liquido implements LiquidoInterface { autoDocumentador: AutoDocumentador; arquivosDelegua: string[]; + arquivosPitugues: string[]; rotasDelegua: string[]; + rotasPitugues: string[]; diretorioBase: string; diretorioDescobertos: string[]; diretorioEstatico: string; @@ -53,25 +56,16 @@ export class Liquido implements LiquidoInterface { this.arquivosAbertos = {}; this.conteudoArquivosAbertos = {}; this.arquivosDelegua = []; + this.arquivosPitugues = []; this.rotasDelegua = []; + this.rotasPitugues = []; this.diretorioDescobertos = []; this.diretorioBase = diretorioBase; this.diretorioEstatico = 'publico'; - this.importador = new Importador(new Lexador(), this.arquivosAbertos, this.conteudoArquivosAbertos, false); - - this.avaliadorSintatico = new AvaliadorSintaticoComImportacao(this.importador); - this.avaliadorSintatico.tiposDeFerramentasExternas = { - liquido: { - lincones: 'módulo', - liquido: 'módulo', - requisicao: 'módulo', - resposta: 'módulo' - } - }; + this.configurarPipelineLinguagem('delegua'); this.formatadorLmht = new FormatadorLmht(this.diretorioBase); - this.interpretador = new InterpretadorLiquido(this.importador, process.cwd(), false, console.log); this.autoDocumentador = new AutoDocumentador(); this.roteador = new Roteador(this.autoDocumentador); this.provedorLincones = new ProvedorLincones(); @@ -80,6 +74,12 @@ export class Liquido implements LiquidoInterface { async iniciar(): Promise { await this.importarArquivoConfiguracao(); + + const linguagemSelecionada = this + .centroConfiguracoes?.liquido?.linguagem || 'delegua'; + + this.configurarPipelineLinguagem(linguagemSelecionada); + this.roteador.configurarArquivosEstaticos(this.diretorioEstatico); this.roteador.iniciarMiddlewares(); await this.importarArquivosRotas(); @@ -95,6 +95,50 @@ export class Liquido implements LiquidoInterface { this.escreverEstilos(); } + private configurarPipelineLinguagem(linguagem: string = 'delegua'): void { + const lexador = linguagem === 'delegua' + ? new Lexador() + : new LexadorPitugues(); + + this.importador = new Importador( + lexador, + this.arquivosAbertos, + this.conteudoArquivosAbertos, + false + ); + + if (linguagem === 'delegua') { + this.interpretador = new InterpretadorLiquido( + this.importador, + process.cwd(), + false, + console.log + ); + this.avaliadorSintatico = new AvaliadorSintaticoDeleguaLiquido( + this.importador + ); + } else { + this.interpretador = new InterpretadorLiquidoPitugues( + this.importador, + process.cwd(), + false, + console.log + ); + this.avaliadorSintatico = new AvaliadorSintaticoPituguesLiquido( + this.importador + ); + } + + this.avaliadorSintatico.tiposDeFerramentasExternas = { + liquido: { + lincones: 'módulo', + liquido: 'módulo', + requisicao: 'módulo', + resposta: 'módulo' + } + }; + } + /** * Método de importação do arquivo `configuracao.delprops`. * @returns void. @@ -177,16 +221,24 @@ export class Liquido implements LiquidoInterface { * Método de descoberta de rotas. Recursivo. * @param diretorio O diretório a ser pesquisado. */ - descobrirRotas(diretorio: string): void { + descobrirRotas(diretorio: string, linguagem: string): void { const listaDeRotas = sistemaDeArquivos.readdirSync(diretorio); const diretorioDescobertos: string[] = []; listaDeRotas.forEach((diretorioOuArquivo) => { const caminhoAbsoluto = caminho.join(diretorio, diretorioOuArquivo); - if (caminhoAbsoluto.endsWith('.delegua')) { - this.arquivosDelegua.push(caminhoAbsoluto); - return; + + if (linguagem === 'delegua') { + if (caminhoAbsoluto.endsWith('.delegua')) { + this.arquivosDelegua.push(caminhoAbsoluto); + return; + } + } else if (linguagem === 'pitugues') { + if (caminhoAbsoluto.endsWith('.pitu')) { + this.arquivosPitugues.push(caminhoAbsoluto); + return; + } } if (sistemaDeArquivos.lstatSync(caminhoAbsoluto).isDirectory()) { @@ -195,7 +247,7 @@ export class Liquido implements LiquidoInterface { }); diretorioDescobertos.forEach((diretorioDescoberto) => { - this.descobrirRotas(diretorioDescoberto); + this.descobrirRotas(diretorioDescoberto, linguagem); }); } @@ -248,98 +300,197 @@ export class Liquido implements LiquidoInterface { * @param {string} caminhoArquivo O caminho do arquivo que está sendo lido * @returns A rota resolvida. */ - resolverCaminhoRota(caminhoArquivo: string): string { + resolverCaminhoRota(caminhoArquivo: string, linguagem: string): string { + const extensaoLinguagem = linguagem === 'delegua' + ? 'delegua' + : 'pitu'; + const partesArquivo = caminhoArquivo.split('rotas'); const rotaResolvida = partesArquivo[1] - .replace('inicial.delegua', '') - .replace('.delegua', '') + .replace(`inicial.${extensaoLinguagem}`, '') + .replace(`.${extensaoLinguagem}`, '') .replace(new RegExp(`\\${caminho.sep}`, 'g'), '/') .replace(new RegExp(`/$`, 'g'), '') .replace(new RegExp(`\\[(.+)\\]`, 'g'), ':$1'); return rotaResolvida; } - async importarArquivosRotas(): Promise { - this.descobrirRotas(caminho.join(this.diretorioBase, 'rotas')); + async analisarArquivo(arquivo: string): Promise { + const retornoImportador = this.importador.importar(arquivo, -1); - for (const arquivo of this.arquivosDelegua) { - const retornoImportador = this.importador.importar(arquivo, -1); - const retornoAvaliadorSintatico = await this.avaliadorSintatico.analisar( + const retornoAvaliadorSintatico = await this + .avaliadorSintatico + .analisar( retornoImportador.retornoLexador, retornoImportador.hashArquivo ); - if (retornoAvaliadorSintatico.erros.length > 0) { - for (const erro of retornoAvaliadorSintatico.erros) { - console.error(`[Linha ${erro.linha}] Erro na rota ${arquivo}: ${erro.message}`); - } - continue; + if (retornoAvaliadorSintatico.erros.length > 0) { + for (const erro of retornoAvaliadorSintatico.erros) { + console.error( + `[Linha ${erro.linha}] Erro na rota ${arquivo}: ${erro.message}` + ); } - // Primeiro passo: coletar todas as declarações de funções (middlewares/handlers) - const funcaoDeclaracoes: Map = new Map(); - for (const declaracao of retornoAvaliadorSintatico.declaracoes) { - if (declaracao instanceof FuncaoDeclaracao) { - funcaoDeclaracoes.set(declaracao.simbolo.lexema, declaracao.funcao); - } + return null; + } + + return retornoAvaliadorSintatico.declaracoes; + } + + private coletarFuncoesDaRota( + declaracoes: Declaracao[] + ): Map { + const funcaoDeclaracoes: Map = new Map(); + + for (const declaracao of declaracoes) { + if (declaracao instanceof FuncaoDeclaracao) { + funcaoDeclaracoes.set(declaracao.simbolo.lexema, declaracao.funcao); } + } - // Segundo passo: processar registros de rotas e resolver referências a funções - for (const declaracao of retornoAvaliadorSintatico.declaracoes) { - // Ignora declarações que não são expressões (ex: Funcao para middlewares) - if (!(declaracao instanceof Expressao)) { - continue; + return funcaoDeclaracoes; + } + + private resolverArgumentosDaRota( + argumentos: any[], + funcoesDeclaradas: Map, + arquivo: string + ): FuncaoConstruto[] { + const argumentosResolvidos: FuncaoConstruto[] = []; + + for (const argumento of argumentos) { + if (argumento instanceof Variavel) { + // Referência a função declarada + const nomeFuncao = argumento.simbolo.lexema; + + if (funcoesDeclaradas.has(nomeFuncao)) { + const funcaoResolvida = funcoesDeclaradas.get(nomeFuncao); + + if (funcaoResolvida) { + argumentosResolvidos.push(funcaoResolvida); + } + } else { + console.error(`Função '${nomeFuncao}' referenciada mas não encontrada em ${arquivo}`); } + } else if (argumento instanceof FuncaoConstruto) { + // Função inline/anônima + argumentosResolvidos.push(argumento); + } else { + console.error( + `Argumento de rota inválido em ${arquivo}: esperado função ou referência a função` + ); + } + } + + return argumentosResolvidos; + } - const expressao: Chamada = declaracao.expressao as Chamada; - const entidadeChamada: AcessoMetodo = expressao.entidadeChamada as AcessoMetodo; - const objeto = entidadeChamada.objeto as Variavel; - - if (objeto.simbolo.lexema.toLowerCase() === 'liquido') { - switch (entidadeChamada.nomeMetodo) { - case 'rotaGet': - case 'rotaPost': - case 'rotaPut': - case 'rotaDelete': - case 'rotaPatch': - case 'rotaOptions': - case 'rotaCopy': - case 'rotaHead': - case 'rotaLock': - case 'rotaUnlock': - case 'rotaPurge': - case 'rotaPropfind': - // Resolve argumentos: converte Variavel em FuncaoConstruto - const argumentosResolvidos: FuncaoConstruto[] = []; - for (const argumento of expressao.argumentos) { - if (argumento instanceof Variavel) { - // Referência a função declarada - const nomeFuncao = argumento.simbolo.lexema; - if (funcaoDeclaracoes.has(nomeFuncao)) { - const funcaoResolvida = funcaoDeclaracoes.get(nomeFuncao); - if (funcaoResolvida) { - argumentosResolvidos.push(funcaoResolvida); - } - } else { - console.error(`Função '${nomeFuncao}' referenciada mas não encontrada em ${arquivo}`); - } - } else if (argumento instanceof FuncaoConstruto) { - // Função inline/anônima - argumentosResolvidos.push(argumento); - } else { - console.error(`Argumento de rota inválido em ${arquivo}: esperado função ou referência a função`); - } + private extrairChamadaLiquido( + declaracao: any + ): { nomeMetodo: string; argumentos: any[] } | null { + const expressao = declaracao instanceof Expressao + ? declaracao.expressao + : null; + + if ( + expressao instanceof Chamada && + expressao.entidadeChamada instanceof AcessoMetodo && + expressao.entidadeChamada.objeto instanceof Variavel && + expressao.entidadeChamada.objeto.simbolo.lexema.toLowerCase() === 'liquido' + ) { + return { + nomeMetodo: expressao.entidadeChamada.nomeMetodo, + argumentos: expressao.argumentos + }; + } + + return null; + } + + async importarArquivosRotas(): Promise { + const metodosRotaPermitidos = new Set([ + 'rotaGet', + 'rotaPost', + 'rotaPut', + 'rotaDelete', + 'rotaPatch', + 'rotaOptions', + 'rotaCopy', + 'rotaHead', + 'rotaLock', + 'rotaUnlock', + 'rotaPurge', + 'rotaPropfind' + ]); + + const linguagemSelecionada = this + .centroConfiguracoes?.liquido?.linguagem || 'delegua'; + + this.descobrirRotas( + caminho.join(this.diretorioBase, 'rotas'), + linguagemSelecionada + ); + + const arquivosParaLer = linguagemSelecionada === 'delegua' + ? this.arquivosDelegua + : this.arquivosPitugues; + + for (const arquivo of arquivosParaLer) { + const declaracoes = await this.analisarArquivo(arquivo); + if (!declaracoes) continue; + + // Primeiro passo: coletar todas as declarações de funções (middlewares/handlers) + const funcaoDeclaracoes = this.coletarFuncoesDaRota(declaracoes); + + // Segundo passo: processar registros de rotas e resolver referências a funções + for (const declaracao of declaracoes) { + // Decoradores em Funções (@liquido.rotaGet) + if ( + declaracao instanceof FuncaoDeclaracao && declaracao.decoradores?.length > 0 + ) { + for (const decorador of declaracao.decoradores) { + const nomeDecorador = decorador.nome.toLowerCase(); + + if (nomeDecorador.startsWith('liquido.rota')) { + const partes = decorador.nome.split('.'); + const nomeMetodo = partes[partes.length - 1]; + + if (metodosRotaPermitidos.has(nomeMetodo)) { + await this.adicionarRota( + nomeMetodo, + this.resolverCaminhoRota( + arquivo, + linguagemSelecionada + ), + [declaracao.funcao] + ); } + } + } + } - await this.adicionarRota( - entidadeChamada.nomeMetodo, - this.resolverCaminhoRota(arquivo), - argumentosResolvidos + // Ignora declarações que não são expressões (ex: Funcao para middlewares) + const chamadaLiquido = this.extrairChamadaLiquido(declaracao); + if (chamadaLiquido) { + const { nomeMetodo, argumentos } = chamadaLiquido; + + if (metodosRotaPermitidos.has(nomeMetodo)) { + const argumentosResolvidos = this + .resolverArgumentosDaRota( + argumentos, + funcaoDeclaracoes, + arquivo ); - break; - default: - console.error(`Método ${entidadeChamada.nomeMetodo} não reconhecido.`); - break; + + await this.adicionarRota( + nomeMetodo, + this.resolverCaminhoRota( + arquivo, + linguagemSelecionada + ), + argumentosResolvidos + ); } } } @@ -732,7 +883,11 @@ export class Liquido implements LiquidoInterface { * para a resolução da rota. O último argumento é o handler final, * todos os anteriores são middlewares executados em sequência. */ - async adicionarRota(metodoRoteador: string, caminhoRota: string, argumentos: FuncaoConstruto[]): Promise { + async adicionarRota( + metodoRoteador: string, + caminhoRota: string, + argumentos: FuncaoConstruto[] + ): Promise { if (argumentos.length === 0) { console.error(`Rota ${caminhoRota} não possui nenhuma função definida.`); return; @@ -750,6 +905,13 @@ export class Liquido implements LiquidoInterface { return; } + const linguagemSelecionada = this.centroConfiguracoes.liquido.linguagem || 'delegua'; + if (linguagemSelecionada === 'delegua') { + this.rotasDelegua.push(caminhoRota); + } else { + this.rotasPitugues.push(caminhoRota); + } + registradorRota(caminhoRota, async (req, res) => { let corpoEStatus: CorpoResposta = {}; diff --git a/testes/exemplos/rotas/inicial.pitu b/testes/exemplos/rotas/inicial.pitu new file mode 100644 index 0000000..83a1615 --- /dev/null +++ b/testes/exemplos/rotas/inicial.pitu @@ -0,0 +1,3 @@ +@liquido.rotaGet("/") +funcao minha_rota_get(requisicao, resposta): + resposta.enviar("Olá, Pituguês").status(200) \ No newline at end of file diff --git a/testes/exemplos/rotas/teste.pitu b/testes/exemplos/rotas/teste.pitu new file mode 100644 index 0000000..e69de29 diff --git a/testes/liquido.test.ts b/testes/liquido.test.ts index eac04b8..f4087e9 100644 --- a/testes/liquido.test.ts +++ b/testes/liquido.test.ts @@ -27,7 +27,10 @@ describe('Liquido', () => { }); it('Testando descobrirRotas()', () => { - liquido.descobrirRotas(caminho.join(__dirname, 'exemplos/rotas')); + liquido.descobrirRotas( + caminho.join(__dirname, 'exemplos/rotas'), + 'delegua' + ); const rota1 = liquido.arquivosDelegua[0].split('rotas')[1]; const rota2 = liquido.arquivosDelegua[1].split('rotas')[1]; expect(liquido.arquivosDelegua.length).toBeGreaterThanOrEqual(2); @@ -38,10 +41,13 @@ describe('Liquido', () => { it('Testando resolverCaminhoRota()', () => { const expected: string[] = []; - liquido.descobrirRotas(caminho.join(__dirname, 'exemplos', 'rotas')); + liquido.descobrirRotas( + caminho.join(__dirname, 'exemplos', 'rotas'), + 'delegua' + ); liquido.arquivosDelegua.forEach((arquivo) => { - expected.push(liquido.resolverCaminhoRota(arquivo)); + expected.push(liquido.resolverCaminhoRota(arquivo, 'delegua')); }); expect(expected.length).toBeGreaterThanOrEqual(2); @@ -58,6 +64,102 @@ describe('Liquido', () => { expect(retorno.caminho).toBe(caminho.join(__dirname, 'exemplos', 'configuracao.delprops')); }); + describe('Suporte a Pituguês', () => { + it('Deve registrar a rota "/" a partir de um decorador no Pituguês', async () => { + const instanciaTeste = new Liquido( + caminho.join(__dirname, 'exemplos') + ); + + (instanciaTeste as any).centroConfiguracoes = { + liquido: { linguagem: 'pitugues', arquetipo: 'rest' } + }; + + jest.spyOn( + instanciaTeste, + 'importarArquivoConfiguracao' + ).mockImplementation(async () => { }); + jest.spyOn( + instanciaTeste.roteador, + 'iniciar' + ).mockImplementation(() => { }); + + await instanciaTeste.iniciar(); + + expect(instanciaTeste.arquivosPitugues.length).toBeGreaterThan(0); + expect(instanciaTeste.rotasPitugues).toContain(''); + + jest.restoreAllMocks(); + }); + + it('Deve analisar e carregar o arquivo inicial.pitu sem erros de sintaxe', async () => { + liquido = new Liquido(caminho.join(__dirname, 'exemplos')); + + (liquido as any).centroConfiguracoes = { + liquido: { linguagem: 'pitugues' } + }; + + jest.spyOn( + liquido, + 'importarArquivoConfiguracao' + ).mockImplementation(async () => { }); + + jest.spyOn( + liquido.roteador, + 'iniciar' + ).mockImplementation(() => { }); + + await expect(liquido.iniciar()).resolves.not.toThrow(); + + expect(liquido.arquivosPitugues.length).toBeGreaterThan(0); + + const arquivoProcessado = liquido.arquivosPitugues.some( + arquivo => arquivo.includes('inicial.pitu') + ); + expect(arquivoProcessado).toBe(true); + }); + + it('Deve descobrir arquivos .pitu recursivamente quando a linguagem for pitugues', () => { + liquido.descobrirRotas( + caminho.join(__dirname, 'exemplos', 'rotas'), + 'pitugues' + ); + + expect(liquido.arquivosPitugues.length).toBeGreaterThan(0); + + liquido.arquivosPitugues.forEach(arquivo => { + expect(arquivo.endsWith('.pitu')).toBe(true); + }); + + expect(liquido.arquivosDelegua.length).toBe(0); + }); + + it('Deve resolver caminho de arquivo inicial.pitu para rota raiz', () => { + const arquivo = caminho.join( + __dirname, + 'exemplos', + 'rotas', + 'inicial.pitu' + ); + const rota = liquido.resolverCaminhoRota(arquivo, 'pitugues'); + + expect(rota).toBe(''); + }); + + it('Deve remover extensão .pitu do caminho', () => { + const arquivo = caminho.join( + __dirname, + 'exemplos', + 'rotas', + 'teste.pitu' + ); + const rota = liquido.resolverCaminhoRota(arquivo, 'pitugues'); + + expect(rota).not.toContain('.pitu'); + + expect(rota.replace(/\\/g, '/')).toBe('/teste'); + }); + }); + describe('Testes de Middlewares', () => { beforeEach(() => { liquido = new Liquido(process.cwd()); @@ -65,7 +167,10 @@ describe('Liquido', () => { it('Deve importar arquivo de rotas com middlewares sem erros', async () => { // Testa que o arquivo com middlewares é importado corretamente - liquido.descobrirRotas(caminho.join(__dirname, 'exemplos', 'rotas')); + liquido.descobrirRotas( + caminho.join(__dirname, 'exemplos', 'rotas'), + 'delegua' + ); // Verifica que o arquivo middlewares.delegua foi descoberto const arquivoMiddlewares = liquido.arquivosDelegua.find( @@ -78,7 +183,10 @@ describe('Liquido', () => { it('Deve processar rotas com diferentes quantidades de middlewares', async () => { // Este teste verifica que as rotas são registradas corretamente // Pode ser expandido para verificar o comportamento específico - liquido.descobrirRotas(caminho.join(__dirname, 'exemplos', 'rotas')); + liquido.descobrirRotas( + caminho.join(__dirname, 'exemplos', 'rotas'), + 'delegua' + ); // Verifica que múltiplos arquivos de rota foram descobertos expect(liquido.arquivosDelegua.length).toBeGreaterThan(0); @@ -92,7 +200,10 @@ describe('Liquido', () => { it('Deve resolver caminhos de rotas com middlewares corretamente', () => { const caminhoTeste = caminho.join(__dirname, 'exemplos', 'rotas', 'middlewares.delegua'); - const caminhoResolvido = liquido.resolverCaminhoRota(caminhoTeste); + const caminhoResolvido = liquido.resolverCaminhoRota( + caminhoTeste, + 'delegua' + ); // O caminho resolvido deve ser '/middlewares' expect(caminhoResolvido).toBe('/middlewares'); @@ -143,6 +254,7 @@ describe('Liquido', () => { await copiarArquivosDeExemploParaNovoProjeto( 'ProjetoLegal', 'api-rest', + 'delegua', caminhoDiretorioProjeto ); @@ -159,6 +271,7 @@ describe('Liquido', () => { await copiarArquivosDeExemploParaNovoProjeto( 'ProjetoLegal', 'mvc', + 'delegua', caminhoDiretorioProjeto ); @@ -180,6 +293,7 @@ describe('Liquido', () => { await copiarArquivosDeExemploParaNovoProjeto( 'ProjetoLegal', 'api-rest', + 'delegua', caminhoDiretorioProjeto ); @@ -240,6 +354,7 @@ describe('Liquido', () => { await copiarArquivosDeExemploParaNovoProjeto( nomeProjetoResolvido, 'api-rest', + 'delegua', caminhoDiretorioProjeto ); diff --git a/testes/middlewares-integracao.test.ts b/testes/middlewares-integracao.test.ts index 485b8bc..9d27c61 100644 --- a/testes/middlewares-integracao.test.ts +++ b/testes/middlewares-integracao.test.ts @@ -7,6 +7,10 @@ describe('Testes de Integração - Middlewares', () => { beforeEach(() => { liquido = new Liquido(caminho.join(__dirname, 'exemplos')); + + (liquido as any).centroConfiguracoes = { + liquido: { linguagem: 'delegua', arquetipo: 'rest' } + }; }); describe('Importação e Parsing de Rotas com Middlewares', () => { @@ -16,7 +20,10 @@ describe('Testes de Integração - Middlewares', () => { }); it('Deve descobrir todas as rotas incluindo middlewares', () => { - liquido.descobrirRotas(caminho.join(__dirname, 'exemplos', 'rotas')); + liquido.descobrirRotas( + caminho.join(__dirname, 'exemplos', 'rotas'), + 'delegua' + ); // Verifica que descobriu múltiplos arquivos expect(liquido.arquivosDelegua.length).toBeGreaterThan(0); @@ -35,7 +42,10 @@ describe('Testes de Integração - Middlewares', () => { 'rotas', 'middlewares.delegua' ); - const caminhoResolvido = liquido.resolverCaminhoRota(arquivoTeste); + const caminhoResolvido = liquido.resolverCaminhoRota( + arquivoTeste, + 'delegua' + ); expect(caminhoResolvido).toBe('/middlewares'); }); @@ -70,7 +80,10 @@ describe('Testes de Integração - Middlewares', () => { }); it('Deve descobrir arquivos .delegua recursivamente', () => { - liquido.descobrirRotas(caminho.join(__dirname, 'exemplos', 'rotas')); + liquido.descobrirRotas( + caminho.join(__dirname, 'exemplos', 'rotas'), + 'delegua' + ); // Deve encontrar múltiplos arquivos expect(liquido.arquivosDelegua.length).toBeGreaterThanOrEqual(3); @@ -82,7 +95,10 @@ describe('Testes de Integração - Middlewares', () => { }); it('Deve descobrir arquivos em subdiretórios', () => { - liquido.descobrirRotas(caminho.join(__dirname, 'exemplos', 'rotas')); + liquido.descobrirRotas( + caminho.join(__dirname, 'exemplos', 'rotas'), + 'delegua' + ); // Deve incluir arquivos do subdiretório mvc const arquivoMvc = liquido.arquivosDelegua.find( @@ -95,28 +111,28 @@ describe('Testes de Integração - Middlewares', () => { describe('Resolução de Caminhos', () => { it('Deve resolver caminho de arquivo inicial.delegua para rota raiz', () => { const arquivo = caminho.join(__dirname, 'exemplos', 'rotas', 'inicial.delegua'); - const rota = liquido.resolverCaminhoRota(arquivo); + const rota = liquido.resolverCaminhoRota(arquivo, 'delegua'); expect(rota).toBe(''); }); it('Deve resolver caminho de arquivo em subdiretório', () => { const arquivo = caminho.join(__dirname, 'exemplos', 'rotas', 'mvc', 'inicial.delegua'); - const rota = liquido.resolverCaminhoRota(arquivo); + const rota = liquido.resolverCaminhoRota(arquivo, 'delegua'); expect(rota).toBe('/mvc'); }); it('Deve remover extensão .delegua do caminho', () => { const arquivo = caminho.join(__dirname, 'exemplos', 'rotas', 'teste.delegua'); - const rota = liquido.resolverCaminhoRota(arquivo); + const rota = liquido.resolverCaminhoRota(arquivo, 'delegua'); expect(rota).not.toContain('.delegua'); }); it('Deve substituir separadores de caminho por barras', () => { const arquivo = caminho.join(__dirname, 'exemplos', 'rotas', 'sub', 'teste.delegua'); - const rota = liquido.resolverCaminhoRota(arquivo); + const rota = liquido.resolverCaminhoRota(arquivo, 'delegua'); // Deve usar barras, não backslashes expect(rota).toContain('/');