diff --git a/fontes/liquido.ts b/fontes/liquido.ts index 587ff66..961f054 100644 --- a/fontes/liquido.ts +++ b/fontes/liquido.ts @@ -6,13 +6,13 @@ import { AcessoMetodo, AcessoMetodoOuPropriedade, Chamada, FuncaoConstruto, Vari import { Expressao, FuncaoDeclaracao } from '@designliquido/delegua/declaracoes' import { DeleguaFuncao, ObjetoDeleguaClasse } from '@designliquido/delegua/interpretador/estruturas'; import { ReferenciaMontao } from '@designliquido/delegua/interpretador/estruturas/referencia-montao'; -import { - ErroInterpretadorInterface, - InterpretadorInterface, - ResultadoParcialInterpretadorInterface, - RetornoInterpretadorInterface, - SimboloInterface, - VariavelInterface +import { + ErroInterpretadorInterface, + InterpretadorInterface, + ResultadoParcialInterpretadorInterface, + RetornoInterpretadorInterface, + SimboloInterface, + VariavelInterface } from '@designliquido/delegua/interfaces'; import { RetornoLexadorInterface } from '@designliquido/delegua/interfaces/retornos/retorno-lexador-interface'; import { InformacaoElementoSintatico } from '@designliquido/delegua/informacao-elemento-sintatico'; @@ -298,8 +298,8 @@ export class Liquido implements LiquidoInterface { this.arquivosDelegua.push(caminhoAbsoluto); continue; } - } - + } + if (linguagemNorm === 'pitugues') { if (caminhoAbsoluto.endsWith('.pitu')) { this.arquivosPitugues.push(caminhoAbsoluto); @@ -395,8 +395,8 @@ export class Liquido implements LiquidoInterface { const retornoImportador = this.importador?.importar(arquivo, -1); const retornoAvaliadorSintatico = await (this - .avaliadorSintatico as { - analisar: (retornoLexador: RetornoLexadorInterface>, hashArquivo: number) => Promise<{ erros: any[]; declaracoes: Declaracao[] }> + .avaliadorSintatico as { + analisar: (retornoLexador: RetornoLexadorInterface>, hashArquivo: number) => Promise<{ erros: any[]; declaracoes: Declaracao[] }> })?.analisar( retornoImportador?.retornoLexador as RetornoLexadorInterface>, retornoImportador?.hashArquivo as number @@ -569,6 +569,32 @@ export class Liquido implements LiquidoInterface { } } + /** + * Normaliza um objeto JavaScript simples que pode ter sido criado sem protótipo + * (como `req.params`, `req.query` do Express, que usam `Object.create(null)`). + * O interpretador Delégua depende de `constructor` para identificar objetos + * como dicionários, então é necessário garantir que o protótipo padrão exista. + */ + private normalizarObjetoJS(valor: any): any { + if ( + valor !== null && + typeof valor === 'object' && + !Array.isArray(valor) + ) { + if (valor.constructor === undefined) { + // Objeto criado sem protótipo (ex: Object.create(null)). + // Cria uma cópia com protótipo padrão para que o interpretador + // consiga acessar `constructor` e tratar o objeto como dicionário. + return { ...valor }; + } + + // Já tem protótipo; mantém o original. + return valor; + } + + return valor; + } + /** * O Interpretador Delégua exige alguns parâmetros definidos antes de executar. * Esse método define esses parâmetros na posição inicial da pilha de execução @@ -581,13 +607,16 @@ export class Liquido implements LiquidoInterface { this.avaliadorSintatico?.pilhaEscopos.definirInformacoesVariavel('liquido', new InformacaoElementoSintatico('liquido', 'módulo')); this.avaliadorSintatico?.pilhaEscopos.definirInformacoesVariavel('requisicao', new InformacaoElementoSintatico('requisicao', 'módulo')); this.avaliadorSintatico?.pilhaEscopos.definirInformacoesVariavel('resposta', new InformacaoElementoSintatico('resposta', 'módulo')); + const descritorClasseRequisicao = new Requisicao(requisicao); await descritorClasseRequisicao.chamar(this.interpretador as InterpretadorInterface, []); + const instanciaRequisicao = new ObjetoDeleguaClasse(descritorClasseRequisicao); - instanciaRequisicao.definir({ lexema: 'corpo' } as SimboloInterface, requisicao.body); - instanciaRequisicao.definir({ lexema: 'parametros' } as SimboloInterface, requisicao.params); - instanciaRequisicao.definir({ lexema: 'parametrosPesquisa' } as SimboloInterface, requisicao.query || {}); + instanciaRequisicao.definir({ lexema: 'corpo' } as SimboloInterface, this.normalizarObjetoJS(requisicao.body)); + instanciaRequisicao.definir({ lexema: 'parametros' } as SimboloInterface, this.normalizarObjetoJS(requisicao.params)); + instanciaRequisicao.definir({ lexema: 'parametrosPesquisa' } as SimboloInterface, this.normalizarObjetoJS(requisicao.query || {})); instanciaRequisicao.definir({ lexema: 'parametrosCaminho' } as SimboloInterface, requisicao.path); + this.interpretador?.pilhaEscoposExecucao.definirVariavel( 'requisicao', instanciaRequisicao @@ -627,7 +656,7 @@ export class Liquido implements LiquidoInterface { ) ], true - ) || { + ) || { erros: [], resultado: [] } as RetornoInterpretadorInterface; diff --git a/testes/e2e/rotas.test.ts b/testes/e2e/rotas.test.ts index 4579b76..55bf374 100644 --- a/testes/e2e/rotas.test.ts +++ b/testes/e2e/rotas.test.ts @@ -60,6 +60,24 @@ describe('E2E Integration Tests - Routing & MIME Types', () => { }); }); + describe('Parâmetros de Rota ([id])', () => { + it('Deve retornar o parâmetro de rota dinâmica via interpolacão', async () => { + const response = await request(liquido.roteador.aplicacao) + .get('/api/clientes/42') + .expect(200); + + expect(response.text).toBe('Teste com ID. 42'); + }); + + it('Deve retornar parâmetros de rota dinâmica como um objeto JSON', async () => { + const response = await request(liquido.roteador.aplicacao) + .post('/api/clientes/99') + .expect(200); + + expect(response.body).toEqual({ id: '99' }); + }); + }); + describe('REST API Routes', () => { it('should return 204 No Content for empty response with no body content', async () => { const response = await request(liquido.roteador.aplicacao) diff --git a/testes/exemplos/rotas/api/clientes/[id].delegua b/testes/exemplos/rotas/api/clientes/[id].delegua new file mode 100644 index 0000000..2ae10d4 --- /dev/null +++ b/testes/exemplos/rotas/api/clientes/[id].delegua @@ -0,0 +1,7 @@ +liquido.rotaGet(funcao(requisicao, resposta) { + resposta.enviar("Teste com ID. ${requisicao.parametros.id}").status(200) +}) + +liquido.rotaPost(funcao(requisicao, resposta) { + resposta.json(requisicao.parametros).status(200) +}) diff --git a/testes/liquido.test.ts b/testes/liquido.test.ts index 56cf9b2..1cb73db 100644 --- a/testes/liquido.test.ts +++ b/testes/liquido.test.ts @@ -440,15 +440,13 @@ describe('Liquido', () => { ); } }); - it('Deve usar o nome da pasta quando o nome do projeto é criado com "." ou "./"', async () => { - const spyCwd = jest .spyOn(process, 'cwd') .mockReturnValue('/home/usuario/minha-pasta-teste'); - let nomeProjetoTerminal = '.'; + const nomeProjetoTerminal = '.'; let nomeProjetoResolvido = nomeProjetoTerminal; if ( @@ -481,6 +479,138 @@ describe('Liquido', () => { }); }); + describe('normalizarObjetoJS', () => { + it('Deve converter Object.create(null) para objeto com protótipo padrão', () => { + const semPrototipo = Object.create(null); + semPrototipo.id = '42'; + semPrototipo.nome = 'teste'; + + const resultado = (liquido as any).normalizarObjetoJS(semPrototipo); + + // Deve ser um objeto comum + expect(typeof resultado).toBe('object'); + expect(resultado).not.toBeNull(); + expect(Array.isArray(resultado)).toBe(false); + + // Deve ter protótipo (constructor definido) + expect(resultado.constructor).toBe(Object); + + // Deve preservar as propriedades originais + expect(resultado.id).toBe('42'); + expect(resultado.nome).toBe('teste'); + + // Deve ser uma cópia, não o mesmo objeto + expect(resultado).not.toBe(semPrototipo); + }); + + it('Deve manter objetos com protótipo padrão inalterados', () => { + const objetoNormal = { id: '42', nome: 'teste' }; + const resultado = (liquido as any).normalizarObjetoJS(objetoNormal); + + // Deve ser o mesmo objeto (sem cópia) + expect(resultado).toBe(objetoNormal); + + // Deve manter as propriedades + expect(resultado.id).toBe('42'); + expect(resultado.nome).toBe('teste'); + + // Deve ter constructor + expect(resultado.constructor).toBe(Object); + }); + + it('Deve retornar null inalterado', () => { + expect((liquido as any).normalizarObjetoJS(null)).toBeNull(); + }); + + it('Deve retornar undefined inalterado', () => { + expect((liquido as any).normalizarObjetoJS(undefined)).toBeUndefined(); + }); + + it('Deve retornar strings inalteradas', () => { + expect((liquido as any).normalizarObjetoJS('texto')).toBe('texto'); + expect((liquido as any).normalizarObjetoJS('')).toBe(''); + }); + + it('Deve retornar números inalterados', () => { + expect((liquido as any).normalizarObjetoJS(42)).toBe(42); + expect((liquido as any).normalizarObjetoJS(0)).toBe(0); + expect((liquido as any).normalizarObjetoJS(-1)).toBe(-1); + }); + + it('Deve retornar booleanos inalterados', () => { + expect((liquido as any).normalizarObjetoJS(true)).toBe(true); + expect((liquido as any).normalizarObjetoJS(false)).toBe(false); + }); + + it('Deve retornar arrays inalterados (arrays têm protótipo próprio)', () => { + const array = [1, 2, 3]; + const resultado = (liquido as any).normalizarObjetoJS(array); + + expect(resultado).toBe(array); + expect(Array.isArray(resultado)).toBe(true); + expect(resultado).toEqual([1, 2, 3]); + }); + + it('Deve converter Object.create(null) aninhado em primeiro nível', () => { + const semPrototipo = Object.create(null); + semPrototipo.chave = 'valor'; + + const resultado = (liquido as any).normalizarObjetoJS(semPrototipo); + + expect(resultado).not.toBe(semPrototipo); + expect(resultado.chave).toBe('valor'); + expect(resultado.constructor).toBe(Object); + }); + + it('Deve funcionar com objetos vazios (sem propriedades)', () => { + const semPrototipoVazio = Object.create(null); + const resultado = (liquido as any).normalizarObjetoJS(semPrototipoVazio); + + expect(resultado).not.toBe(semPrototipoVazio); + expect(resultado.constructor).toBe(Object); + expect(Object.keys(resultado).length).toBe(0); + }); + + it('Deve converter Object.create(null) simulando req.params do Express', () => { + // Express cria req.params com Object.create(null) + const reqParams = Object.create(null); + reqParams.id = '42'; + reqParams.categoria = 'livros'; + + const resultado = (liquido as any).normalizarObjetoJS(reqParams); + + // Verifica se o interpretador consegue acessar .constructor + expect(resultado.constructor).toBe(Object); + expect(resultado.constructor.name).toBe('Object'); + + // Verifica os valores + expect(resultado.id).toBe('42'); + expect(resultado.categoria).toBe('livros'); + }); + + it('Deve preservar datas e outros objetos com protótipo', () => { + const data = new Date('2024-01-01'); + const resultado = (liquido as any).normalizarObjetoJS(data); + + // Date tem constructor, deve ser mantido como está + expect(resultado).toBe(data); + expect(resultado.constructor).toBe(Date); + }); + + it('Deve preservar objetos com protótipo customizado', () => { + class MeuObjeto { + constructor(public valor: number) {} + } + const objeto = new MeuObjeto(42); + + const resultado = (liquido as any).normalizarObjetoJS(objeto); + + // Deve ser mantido como está (tem constructor) + expect(resultado).toBe(objeto); + expect(resultado.valor).toBe(42); + }); + }); + describe('Processamento de Respostas', () => { it('Deve limpar objetos Delégua e retornar JSON puro', async () => { const objetoSimulado = {