IDENTIFICATION DIVISION. PROGRAM-ID. CEPIS-LOOKUP. *> --------------------------------------------------------------------- *> Look up one CEP. *> *> CALL "CEPIS-LOOKUP" USING CEPIS-CONFIG CEPIS-CEP-INPUT *> CEPIS-ADDRESS CEPIS-STATUS *> *> The CEP may be masked or not. It is validated locally first, so a *> seven-digit CEP costs no round trip: CEPIS-INVALID-CEP comes back *> without a socket ever being opened. *> *> On CEPIS-OK the address is filled. On anything else CEPIS-ADDRESS is *> cleared, so a caller that forgets to test the status reads spaces *> rather than the previous CEP's street. *> *> ADDR-GEO-PRESENT says whether the coordinate exists -- about 57% of *> CEPs have one. When it does, read ADDR-GEO-PRECISAO before measuring *> any distance with it: "logradouro" means the point is a stretch of *> street, with ADDR-GEO-RAIO-M around 100-150 m, while "localidade" *> means the CEP is a whole municipality and the point is its centroid, *> with a radius of 7 to 40 km. Computing shipping from the second is *> wrong by tens of kilometres. *> *> A CEPIS-NOT-FOUND may come back with CEPIS-FAIXA filled: the *> municipality the CEP would have belonged to. A CEP that fails is *> rarely a random number -- it is usually someone who knows the city and *> guessed the digits, and the classic guess is the -000 that 957 *> municipalities never received. The lookup has still failed and the *> address is still empty; CEPIS-FAIXA only names where the guess landed. *> --------------------------------------------------------------------- DATA DIVISION. WORKING-STORAGE SECTION. COPY "cepis-http.cpy". COPY "cepis-json.cpy". 01 WS-CEP PIC X(8). 01 WS-GEO-FROM PIC 9(9). 01 WS-GEO-TO PIC 9(9). 01 WS-GEO-OK PIC X. 01 WS-NUM-OK PIC X. 01 WS-LAT PIC S9(3)V9(6). 01 WS-LON PIC S9(3)V9(6). 01 WS-RAIO PIC 9(9). 01 WS-PRECISAO PIC X(16). 01 WS-FAIXA-FROM PIC 9(9). 01 WS-FAIXA-TO PIC 9(9). 01 WS-FAIXA-OK PIC X. 01 WS-F-LOCALIDADE PIC X(100). 01 WS-F-UF PIC X(2). 01 WS-F-ESTADO PIC X(60). 01 WS-F-REGIAO PIC X(30). 01 WS-F-IBGE PIC X(10). 01 WS-F-CEP-INI PIC X(8). 01 WS-F-CEP-FIM PIC X(8). 01 WS-F-CEPS PIC 9(9). 01 WS-I PIC 9(4). 01 WS-C PIC X. 01 WS-DOTS PIC 9(4). 01 WS-JUNK PIC 9(4). 01 WS-DIGITS PIC 9(4). LINKAGE SECTION. COPY "cepis.cpy". PROCEDURE DIVISION USING CEPIS-CONFIG CEPIS-CEP-INPUT CEPIS-ADDRESS CEPIS-STATUS. MAIN-START. PERFORM CLEAR-ADDRESS CALL "CEPIS-NORMALIZE-CEP" USING CEPIS-CEP-INPUT WS-CEP CEPIS-STATUS END-CALL IF CEPIS-FAILED GOBACK END-IF MOVE SPACES TO HTTPQ-PATH STRING "/" WS-CEP DELIMITED BY SIZE INTO HTTPQ-PATH END-STRING MOVE 9 TO HTTPQ-PATH-LEN MOVE "N" TO HTTPQ-ALLOW-503 CALL "CEPIS-REQUEST" USING CEPIS-CONFIG CEPIS-HTTP-REQUEST CEPIS-HTTP-RESPONSE CEPIS-STATUS END-CALL IF CEPIS-FAILED GOBACK END-IF EVALUATE HTTP-STATUS WHEN 200 PERFORM PARSE-ADDRESS WHEN 404 SET CEPIS-NOT-FOUND TO TRUE MOVE SPACES TO CEPIS-STATUS-MESSAGE STRING "CEP " WS-CEP " does not exist in the DNE database" DELIMITED BY SIZE INTO CEPIS-STATUS-MESSAGE END-STRING PERFORM PARSE-FAIXA WHEN 400 SET CEPIS-INVALID-CEP TO TRUE PERFORM API-MESSAGE WHEN OTHER SET CEPIS-INVALID-RESPONSE TO TRUE MOVE SPACES TO CEPIS-STATUS-MESSAGE STRING "unexpected status " HTTP-STATUS " for CEP " WS-CEP DELIMITED BY SIZE INTO CEPIS-STATUS-MESSAGE END-STRING END-EVALUATE GOBACK. *> ------------------------------------------------------------------- CLEAR-ADDRESS. INITIALIZE CEPIS-ADDRESS SET ADDR-NOT-TRUNCATED TO TRUE SET ADDR-GEO-ABSENT TO TRUE MOVE SPACES TO ADDR-GEO-PRECISAO. *> ------------------------------------------------------------------- *> The API's own 400 carries a message this client does not have: the *> format already passed local validation, so the server knows something *> we do not. Surface its "mensagem" when there is one. API-MESSAGE. MOVE SPACES TO CEPIS-STATUS-MESSAGE MOVE "mensagem" TO JS-KEY PERFORM GET-MEMBER IF JS-TYPE = "S" AND JS-VALUE-LEN > 0 MOVE JS-VALUE TO CEPIS-STATUS-MESSAGE ELSE STRING "CEP " WS-CEP " rejected by the API" DELIMITED BY SIZE INTO CEPIS-STATUS-MESSAGE END-STRING END-IF. *> ------------------------------------------------------------------- GET-MEMBER. MOVE 1 TO JS-FROM MOVE HTTP-BODY-LEN TO JS-TO CALL "CEPIS-JSON-GET" USING HTTP-BODY CEPIS-JSON-SCAN END-CALL. *> ------------------------------------------------------------------- GET-GEO-MEMBER. MOVE WS-GEO-FROM TO JS-FROM MOVE WS-GEO-TO TO JS-TO CALL "CEPIS-JSON-GET" USING HTTP-BODY CEPIS-JSON-SCAN END-CALL. *> ------------------------------------------------------------------- GET-FAIXA-MEMBER. MOVE WS-FAIXA-FROM TO JS-FROM MOVE WS-FAIXA-TO TO JS-TO CALL "CEPIS-JSON-GET" USING HTTP-BODY CEPIS-JSON-SCAN END-CALL. *> ------------------------------------------------------------------- PARSE-ADDRESS. MOVE "cep" TO JS-KEY PERFORM GET-MEMBER IF JS-TYPE = "E" SET CEPIS-INVALID-RESPONSE TO TRUE MOVE "response is not a JSON object -- something between you " & "and the API is answering instead of it" TO CEPIS-STATUS-MESSAGE EXIT PARAGRAPH END-IF IF JS-VALUE-LEN > 8 SET ADDR-WAS-TRUNCATED TO TRUE END-IF MOVE JS-VALUE TO ADDR-CEP IF ADDR-CEP = SPACES MOVE WS-CEP TO ADDR-CEP END-IF STRING ADDR-CEP(1:5) "-" ADDR-CEP(6:3) DELIMITED BY SIZE INTO ADDR-CEP-MASKED END-STRING MOVE "logradouro" TO JS-KEY PERFORM GET-MEMBER IF JS-VALUE-LEN > 120 SET ADDR-WAS-TRUNCATED TO TRUE END-IF MOVE JS-VALUE TO ADDR-LOGRADOURO MOVE "complemento" TO JS-KEY PERFORM GET-MEMBER IF JS-VALUE-LEN > 120 SET ADDR-WAS-TRUNCATED TO TRUE END-IF MOVE JS-VALUE TO ADDR-COMPLEMENTO MOVE "unidade" TO JS-KEY PERFORM GET-MEMBER IF JS-VALUE-LEN > 80 SET ADDR-WAS-TRUNCATED TO TRUE END-IF MOVE JS-VALUE TO ADDR-UNIDADE MOVE "bairro" TO JS-KEY PERFORM GET-MEMBER IF JS-VALUE-LEN > 100 SET ADDR-WAS-TRUNCATED TO TRUE END-IF MOVE JS-VALUE TO ADDR-BAIRRO MOVE "localidade" TO JS-KEY PERFORM GET-MEMBER IF JS-VALUE-LEN > 100 SET ADDR-WAS-TRUNCATED TO TRUE END-IF MOVE JS-VALUE TO ADDR-LOCALIDADE MOVE "uf" TO JS-KEY PERFORM GET-MEMBER IF JS-VALUE-LEN > 2 SET ADDR-WAS-TRUNCATED TO TRUE END-IF MOVE JS-VALUE TO ADDR-UF MOVE "estado" TO JS-KEY PERFORM GET-MEMBER IF JS-VALUE-LEN > 60 SET ADDR-WAS-TRUNCATED TO TRUE END-IF MOVE JS-VALUE TO ADDR-ESTADO MOVE "regiao" TO JS-KEY PERFORM GET-MEMBER IF JS-VALUE-LEN > 30 SET ADDR-WAS-TRUNCATED TO TRUE END-IF MOVE JS-VALUE TO ADDR-REGIAO MOVE "ibge" TO JS-KEY PERFORM GET-MEMBER MOVE JS-VALUE TO ADDR-IBGE MOVE "gia" TO JS-KEY PERFORM GET-MEMBER MOVE JS-VALUE TO ADDR-GIA MOVE "ddd" TO JS-KEY PERFORM GET-MEMBER MOVE JS-VALUE TO ADDR-DDD MOVE "siafi" TO JS-KEY PERFORM GET-MEMBER MOVE JS-VALUE TO ADDR-SIAFI PERFORM PARSE-GEO SET CEPIS-OK TO TRUE. *> ------------------------------------------------------------------- *> The coordinate is omitted entirely when the database has none, so its *> absence is normal, not an error. A geo block that is present but *> malformed is dropped the same way: the rest of the address is still *> useful and losing it over a bad float would be a poor trade. PARSE-GEO. SET ADDR-GEO-ABSENT TO TRUE MOVE "geo" TO JS-KEY PERFORM GET-MEMBER IF JS-TYPE NOT = "O" EXIT PARAGRAPH END-IF MOVE JS-VSTART TO WS-GEO-FROM MOVE JS-VEND TO WS-GEO-TO MOVE "Y" TO WS-GEO-OK MOVE "lat" TO JS-KEY PERFORM GET-GEO-MEMBER PERFORM CHECK-NUMERIC IF WS-NUM-OK = "Y" COMPUTE WS-LAT = FUNCTION NUMVAL (JS-VALUE) ELSE MOVE "N" TO WS-GEO-OK END-IF MOVE "lon" TO JS-KEY PERFORM GET-GEO-MEMBER PERFORM CHECK-NUMERIC IF WS-NUM-OK = "Y" COMPUTE WS-LON = FUNCTION NUMVAL (JS-VALUE) ELSE MOVE "N" TO WS-GEO-OK END-IF MOVE "raio_m" TO JS-KEY PERFORM GET-GEO-MEMBER PERFORM CHECK-NUMERIC IF WS-NUM-OK = "Y" COMPUTE WS-RAIO ROUNDED = FUNCTION NUMVAL (JS-VALUE) ELSE MOVE "N" TO WS-GEO-OK END-IF MOVE "precisao" TO JS-KEY PERFORM GET-GEO-MEMBER IF JS-TYPE = "S" AND JS-VALUE-LEN > 0 MOVE JS-VALUE TO WS-PRECISAO ELSE MOVE "N" TO WS-GEO-OK END-IF IF WS-GEO-OK = "Y" SET ADDR-GEO-KNOWN TO TRUE MOVE WS-LAT TO ADDR-GEO-LAT MOVE WS-LON TO ADDR-GEO-LON MOVE WS-RAIO TO ADDR-GEO-RAIO-M MOVE WS-PRECISAO TO ADDR-GEO-PRECISAO END-IF. *> ------------------------------------------------------------------- *> The 404 body may name the municipality the CEP would have belonged *> to. It is context for the error and nothing else: the status stays *> CEPIS-NOT-FOUND and CEPIS-ADDRESS stays empty, because a CEP that the *> DNE does not have is not an address no matter which city it is near. *> *> "faixa" is omitted -- never sent as null -- when the CEP falls in a *> band no municipality owns, so its absence is the normal case, not an *> error. Everything here is therefore best effort: a faixa that is *> missing, is not an object, or is short of the members below is *> dropped, and the caller still gets a clean not-found. Turning a 404 *> the caller already understands into CEPIS-INVALID-RESPONSE over a *> decoration would be a poor trade. *> *> cep_ini and cep_fim must both be eight digits and ceps must be a *> number, or the whole block is dropped. The span is sparse -- Areal's *> 118 CEPs are spread over the ~4900 numbers between 25845001 and *> 25849899 -- so handing a caller the pair without the count invites *> reading it as a list of valid CEPs, which it is not. PARSE-FAIXA. INITIALIZE CEPIS-FAIXA SET CEPIS-FAIXA-ABSENT TO TRUE MOVE "faixa" TO JS-KEY PERFORM GET-MEMBER IF JS-TYPE NOT = "O" EXIT PARAGRAPH END-IF MOVE JS-VSTART TO WS-FAIXA-FROM MOVE JS-VEND TO WS-FAIXA-TO MOVE "Y" TO WS-FAIXA-OK MOVE "localidade" TO JS-KEY PERFORM GET-FAIXA-MEMBER IF JS-TYPE = "S" AND JS-VALUE-LEN > 0 MOVE JS-VALUE TO WS-F-LOCALIDADE ELSE MOVE "N" TO WS-FAIXA-OK END-IF MOVE "uf" TO JS-KEY PERFORM GET-FAIXA-MEMBER IF JS-TYPE = "S" AND JS-VALUE-LEN = 2 MOVE JS-VALUE TO WS-F-UF ELSE MOVE "N" TO WS-FAIXA-OK END-IF MOVE "cep_ini" TO JS-KEY PERFORM GET-FAIXA-MEMBER IF JS-TYPE = "S" AND JS-VALUE-LEN = 8 MOVE JS-VALUE TO WS-F-CEP-INI ELSE MOVE "N" TO WS-FAIXA-OK END-IF MOVE "cep_fim" TO JS-KEY PERFORM GET-FAIXA-MEMBER IF JS-TYPE = "S" AND JS-VALUE-LEN = 8 MOVE JS-VALUE TO WS-F-CEP-FIM ELSE MOVE "N" TO WS-FAIXA-OK END-IF MOVE "ceps" TO JS-KEY PERFORM GET-FAIXA-MEMBER PERFORM CHECK-NUMERIC IF WS-NUM-OK = "Y" COMPUTE WS-F-CEPS ROUNDED = FUNCTION NUMVAL (JS-VALUE) ELSE MOVE "N" TO WS-FAIXA-OK END-IF IF WS-FAIXA-OK NOT = "Y" EXIT PARAGRAPH END-IF *> The three below are decoration: a faixa the API sends without *> them still names the municipality, so they never fail the block. MOVE SPACES TO WS-F-ESTADO MOVE "estado" TO JS-KEY PERFORM GET-FAIXA-MEMBER IF JS-TYPE = "S" MOVE JS-VALUE TO WS-F-ESTADO END-IF MOVE SPACES TO WS-F-REGIAO MOVE "regiao" TO JS-KEY PERFORM GET-FAIXA-MEMBER IF JS-TYPE = "S" MOVE JS-VALUE TO WS-F-REGIAO END-IF MOVE SPACES TO WS-F-IBGE MOVE "ibge" TO JS-KEY PERFORM GET-FAIXA-MEMBER IF JS-TYPE = "S" MOVE JS-VALUE TO WS-F-IBGE END-IF SET CEPIS-FAIXA-KNOWN TO TRUE MOVE WS-F-LOCALIDADE TO CEPIS-FAIXA-LOCALIDADE MOVE WS-F-UF TO CEPIS-FAIXA-UF MOVE WS-F-ESTADO TO CEPIS-FAIXA-ESTADO MOVE WS-F-REGIAO TO CEPIS-FAIXA-REGIAO MOVE WS-F-IBGE TO CEPIS-FAIXA-IBGE MOVE WS-F-CEP-INI TO CEPIS-FAIXA-CEP-INI MOVE WS-F-CEP-FIM TO CEPIS-FAIXA-CEP-FIM MOVE WS-F-CEPS TO CEPIS-FAIXA-CEPS. *> ------------------------------------------------------------------- *> FUNCTION NUMVAL answers 0 for text it cannot read, which would turn *> "lat": "not a number" into a coordinate off the coast of Africa. The *> text is therefore checked before it is converted. CHECK-NUMERIC. MOVE "N" TO WS-NUM-OK IF JS-TYPE NOT = "N" AND JS-TYPE NOT = "S" EXIT PARAGRAPH END-IF IF JS-VALUE-LEN = 0 OR JS-VALUE-LEN > 32 EXIT PARAGRAPH END-IF MOVE 0 TO WS-DIGITS MOVE 0 TO WS-DOTS MOVE 0 TO WS-JUNK PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > JS-VALUE-LEN MOVE JS-VALUE(WS-I:1) TO WS-C EVALUATE TRUE WHEN WS-C >= "0" AND WS-C <= "9" ADD 1 TO WS-DIGITS WHEN WS-C = "." ADD 1 TO WS-DOTS WHEN (WS-C = "-" OR WS-C = "+") AND WS-I = 1 CONTINUE WHEN OTHER ADD 1 TO WS-JUNK END-EVALUATE END-PERFORM IF WS-DIGITS > 0 AND WS-DOTS <= 1 AND WS-JUNK = 0 MOVE "Y" TO WS-NUM-OK END-IF. END PROGRAM CEPIS-LOOKUP.