﻿{
  CSCoder.CEP
  Claudio Santos | CS Coder | https://cscoder.com.br/
  SPDX-License-Identifier: MIT

  Uso, modificacao e redistribuicao permitidos, inclusive em projetos
  comerciais. Preserve o aviso de autoria e o texto da licenca.
  Fornecido sem garantias, conforme a licenca abaixo.

  MIT License
  
  Copyright (c) 2026 Claudio Santos (CS Coder)
  
  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files (the "Software"), to deal
  in the Software without restriction, including without limitation the rights
  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  copies of the Software, and to permit persons to whom the Software is
  furnished to do so, subject to the following conditions:
  
  The above copyright notice and this permission notice shall be included in
  all copies or substantial portions of the Software.
  
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  THE SOFTWARE.
}

unit CSCoder.CEP;

interface

type
  TStatusCEP = (scEncontrado, scCEPInvalido, scNaoEncontrado,
    scFalhaHTTP, scFalhaComunicacao, scRespostaInvalida);
  TEnderecoCEP = record
    CEP, Logradouro, Complemento, Bairro, Localidade, UF, IBGE, DDD: string;
  end;
  TResultadoCEP = record
    Status: TStatusCEP;
    Endereco: TEnderecoCEP;
    StatusHTTP: Integer;
    Mensagem: string;
  end;

function NormalizarCEP(const Valor: string; out CEP: string): Boolean;
// Interpretacao separada para testes sem rede e uso com outro transporte.
function InterpretarRespostaCEP(const CEP, Corpo: string;
  const StatusHTTP: Integer): TResultadoCEP;
// Sincrona: nao execute na thread de interface de uma tela.
function ConsultarCEP(const CEP: string): TResultadoCEP;

implementation

uses System.SysUtils, System.Classes, System.JSON, System.Net.HttpClient ;

function NormalizarCEP(const Valor: string; out CEP: string): Boolean;
var S: string; I: Integer;
begin
  CEP := '';
  S := Trim(Valor);
  if Length(S) = 9 then
  begin
    if S[6] <> '-' then Exit(False);
    Delete(S, 6, 1);
  end;
  if Length(S) <> 8 then Exit(False);
  for I := 1 to 8 do
    if not CharInSet(S[I], ['0'..'9']) then Exit(False);
  CEP := S;
  Result := True;
end;

function LerString(const Obj: TJSONObject; const Campo: string;
  out Valor: string): Boolean;
var V: TJSONValue;
begin
  Valor := '';
  V := Obj.GetValue(Campo);
  Result := V is TJSONString;
  if Result then Valor := TJSONString(V).Value;
end;

function LerBoolean(const Obj: TJSONObject; const Campo: string;
  out Valor: Boolean): Boolean;
var V: TJSONValue;
begin
  Valor := False;
  V := Obj.GetValue(Campo);
  Result := True;
  if V is TJSONTrue then Valor := True
  else if V is TJSONFalse then Valor := False
  else if V is TJSONBool then Valor := TJSONBool(V).AsBoolean
  else Result := False;
end;

function LerOpcional(const Obj: TJSONObject; const Campo: string;
  out Valor: string): Boolean;
begin
  Valor := '';
  if Obj.GetValue(Campo) = nil then Exit(True);
  Result := LerString(Obj, Campo, Valor);
end;

function InterpretarRespostaCEP(const CEP, Corpo: string;
  const StatusHTTP: Integer): TResultadoCEP;
var Normalizado, Retornado, TextoErro: string; Raiz: TJSONValue;
  Obj: TJSONObject; V: TJSONValue; Erro: Boolean;
begin
  Result := Default(TResultadoCEP);
  Result.Status := scCEPInvalido;
  Result.Mensagem := 'Informe oito numeros ou a mascara 00000-000.';
  if not NormalizarCEP(CEP, Normalizado) then Exit;
  Result.StatusHTTP := StatusHTTP;
  Result.Status := scFalhaHTTP;
  Result.Mensagem := 'O servico respondeu com um status HTTP inesperado.';
  if StatusHTTP <> 200 then Exit;
  Result.Status := scRespostaInvalida;
  Result.Mensagem := 'A resposta do servico nao atende ao contrato esperado.';
  if (Corpo = '') or (Length(Corpo) > 65536) then Exit;
  Raiz := TJSONObject.ParseJSONValue(Corpo);
  try
    if not (Raiz is TJSONObject) then Exit;
    Obj := TJSONObject(Raiz);
    V := Obj.GetValue('erro');
    if V <> nil then
    begin
      if not LerBoolean(Obj, 'erro', Erro) then
      begin
        // ViaCEP tambem pode devolver erro como a string "true".
        if not LerString(Obj, 'erro', TextoErro) then Exit;
        if (TextoErro <> 'true') and (TextoErro <> 'false') then Exit;
        Erro := TextoErro = 'true';
      end;
      if Erro then
      begin
        Result.Status := scNaoEncontrado;
        Result.Mensagem := 'CEP nao encontrado.';
        Exit;
      end;
    end;
    if not LerString(Obj, 'cep', Result.Endereco.CEP) then Exit;
    if not NormalizarCEP(Result.Endereco.CEP, Retornado) then Exit;
    if Retornado <> Normalizado then Exit;
    if not LerString(Obj, 'localidade', Result.Endereco.Localidade) then Exit;
    if Trim(Result.Endereco.Localidade) = '' then Exit;
    if not LerString(Obj, 'uf', Result.Endereco.UF) then Exit;
    if Length(Result.Endereco.UF) <> 2 then Exit;
    if not CharInSet(Result.Endereco.UF[1], ['A'..'Z']) then Exit;
    if not CharInSet(Result.Endereco.UF[2], ['A'..'Z']) then Exit;
    if not LerOpcional(Obj, 'logradouro', Result.Endereco.Logradouro) then Exit;
    if not LerOpcional(Obj, 'complemento', Result.Endereco.Complemento) then Exit;
    if not LerOpcional(Obj, 'bairro', Result.Endereco.Bairro) then Exit;
    if not LerOpcional(Obj, 'ibge', Result.Endereco.IBGE) then Exit;
    if not LerOpcional(Obj, 'ddd', Result.Endereco.DDD) then Exit;
    Result.Endereco.CEP := Copy(Normalizado, 1, 5) + '-' + Copy(Normalizado, 6, 3);
    Result.Status := scEncontrado;
    Result.Mensagem := 'CEP encontrado.';
  finally
    Raiz.Free;
    if Result.Status <> scEncontrado then
      Result.Endereco := Default(TEnderecoCEP);
  end;
end;

function ConsultarCEP(const CEP: string): TResultadoCEP;
var Normalizado: string; Cliente: THTTPClient; Resposta: IHTTPResponse;
begin
  Result := Default(TResultadoCEP);
  Result.Status := scCEPInvalido;
  Result.Mensagem := 'Informe oito numeros ou a mascara 00000-000.';
  if not NormalizarCEP(CEP, Normalizado) then Exit;
  Cliente := THTTPClient.Create;
  try
    Cliente.ConnectionTimeout := 4000;
    Cliente.ResponseTimeout := 8000;
    try
      Resposta := Cliente.Get('https://viacep.com.br/ws/' + Normalizado + '/json/');
      Result := InterpretarRespostaCEP(Normalizado,
        Resposta.ContentAsString(TEncoding.UTF8), Resposta.StatusCode);
    except
      on E: Exception do
      begin
        Result := Default(TResultadoCEP);
        Result.Status := scFalhaComunicacao;
        Result.Mensagem := 'Nao foi possivel concluir a consulta de CEP.';
      end;
    end;
  finally
    Cliente.Free;
  end;
end;

end.
