Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 44 additions & 15 deletions fontes/liquido.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
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';
Expand Down Expand Up @@ -298,8 +298,8 @@
this.arquivosDelegua.push(caminhoAbsoluto);
continue;
}
}
}

if (linguagemNorm === 'pitugues') {
if (caminhoAbsoluto.endsWith('.pitu')) {
this.arquivosPitugues.push(caminhoAbsoluto);
Expand Down Expand Up @@ -395,8 +395,8 @@
const retornoImportador = this.importador?.importar(arquivo, -1);

const retornoAvaliadorSintatico = await (this
.avaliadorSintatico as {
analisar: (retornoLexador: RetornoLexadorInterface<SimboloInterface<string>>, hashArquivo: number) => Promise<{ erros: any[]; declaracoes: Declaracao[] }>
.avaliadorSintatico as {
analisar: (retornoLexador: RetornoLexadorInterface<SimboloInterface<string>>, hashArquivo: number) => Promise<{ erros: any[]; declaracoes: Declaracao[] }>
})?.analisar(
retornoImportador?.retornoLexador as RetornoLexadorInterface<SimboloInterface<string>>,
retornoImportador?.hashArquivo as number
Expand Down Expand Up @@ -569,6 +569,32 @@
}
}

/**
* 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
Expand All @@ -581,13 +607,16 @@
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 || {}));

Check warning on line 617 in fontes/liquido.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
instanciaRequisicao.definir({ lexema: 'parametrosCaminho' } as SimboloInterface, requisicao.path);

this.interpretador?.pilhaEscoposExecucao.definirVariavel(
'requisicao',
instanciaRequisicao
Expand Down Expand Up @@ -627,10 +656,10 @@
)
],
true
) || {
) || {
erros: [],
resultado: []
} as RetornoInterpretadorInterface;

Check warning on line 662 in fontes/liquido.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
} catch (erro: any) {
console.error(erro);
throw erro;
Expand Down
18 changes: 18 additions & 0 deletions testes/e2e/rotas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions testes/exemplos/rotas/api/clientes/[id].delegua
Original file line number Diff line number Diff line change
@@ -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)
})
136 changes: 133 additions & 3 deletions testes/liquido.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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 = {
Expand Down
Loading