diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 12:49:48 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 12:49:48 -0300 |
| commit | f979d07d23098b96e758b3feb555cd06d85d9b2d (patch) | |
| tree | 47298197a17a3d6c438a1f4cc7cb61afd390cf05 /src | |
| download | cobol-f979d07d23098b96e758b3feb555cd06d85d9b2d.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'src')
| -rw-r--r-- | src/cepis-config.cob | 38 | ||||
| -rw-r--r-- | src/cepis-csv.cob | 124 | ||||
| -rw-r--r-- | src/cepis-health.cob | 93 | ||||
| -rw-r--r-- | src/cepis-http.c | 263 | ||||
| -rw-r--r-- | src/cepis-json.cob | 409 | ||||
| -rw-r--r-- | src/cepis-lookup-many.cob | 346 | ||||
| -rw-r--r-- | src/cepis-lookup.cob | 324 | ||||
| -rw-r--r-- | src/cepis-normalize-cep.cob | 74 | ||||
| -rw-r--r-- | src/cepis-request.cob | 363 |
9 files changed, 2034 insertions, 0 deletions
diff --git a/src/cepis-config.cob b/src/cepis-config.cob new file mode 100644 index 0000000..0f17fd1 --- /dev/null +++ b/src/cepis-config.cob @@ -0,0 +1,38 @@ +IDENTIFICATION DIVISION. +PROGRAM-ID. CEPIS-DEFAULT-CONFIG. + +*> --------------------------------------------------------------------- +*> Fill CEPIS-CONFIG with the shipped defaults. +*> +*> CALL "CEPIS-DEFAULT-CONFIG" USING CEPIS-CONFIG +*> +*> Call this before anything else, then override what you need. Every +*> field is written, so a record left over from another use is safe to +*> pass in. CFG-BASE-URL is the one that usually differs between +*> environments: point it at a local instance and the whole library +*> follows. +*> --------------------------------------------------------------------- + +DATA DIVISION. +WORKING-STORAGE SECTION. +01 CEPIS-VERSION PIC X(5) VALUE "0.1.0". + +LINKAGE SECTION. +COPY "cepis.cpy". + +PROCEDURE DIVISION USING CEPIS-CONFIG. + +MAIN-START. + MOVE "https://cep.is" TO CFG-BASE-URL + MOVE 10000 TO CFG-TIMEOUT-MS + MOVE 3 TO CFG-MAX-ATTEMPTS + MOVE 500 TO CFG-BACKOFF-BASE-MS + MOVE 8000 TO CFG-BACKOFF-MAX-MS + MOVE 0 TO CFG-TOTAL-TIMEOUT-MS + MOVE SPACES TO CFG-USER-AGENT + STRING "cepis-cobol/" CEPIS-VERSION " (+https://cep.is)" + DELIMITED BY SIZE INTO CFG-USER-AGENT + END-STRING + GOBACK. + +END PROGRAM CEPIS-DEFAULT-CONFIG. diff --git a/src/cepis-csv.cob b/src/cepis-csv.cob new file mode 100644 index 0000000..29cc6d2 --- /dev/null +++ b/src/cepis-csv.cob @@ -0,0 +1,124 @@ +IDENTIFICATION DIVISION. +PROGRAM-ID. CEPIS-CSV-NEXT. + +*> --------------------------------------------------------------------- +*> Read one field of an RFC 4180 record. +*> +*> CALL "CEPIS-CSV-NEXT" USING CSV-TEXT CEPIS-CSV-SCAN +*> +*> Values from the batch endpoint contain commas -- "Rua Um, Dois" is an +*> ordinary street name -- and are then double-quoted with "" for an +*> embedded quote. Splitting on commas would shear those rows in two and +*> shift every column after them, so this walks the quoting properly, +*> newlines inside a quoted value included. +*> --------------------------------------------------------------------- + +DATA DIVISION. +WORKING-STORAGE SECTION. +01 WS-C PIC X. +01 WS-DOUBLED PIC X. +01 CONST-QUOTE PIC X VALUE '"'. + +LINKAGE SECTION. +01 CSV-TEXT PIC X(262144). +COPY "cepis-csv.cpy". + +PROCEDURE DIVISION USING CSV-TEXT CEPIS-CSV-SCAN. + +MAIN-START. + MOVE SPACES TO CSV-FIELD + MOVE 0 TO CSV-FIELD-LEN + MOVE "N" TO CSV-ROW-END + MOVE "N" TO CSV-EXHAUSTED + + IF CSV-POS > CSV-LEN + MOVE "Y" TO CSV-EXHAUSTED + MOVE "Y" TO CSV-ROW-END + GOBACK + END-IF + + IF CSV-TEXT(CSV-POS:1) = CONST-QUOTE + PERFORM READ-QUOTED + ELSE + PERFORM READ-BARE + END-IF + + PERFORM CLOSE-FIELD + GOBACK. + +*> ------------------------------------------------------------------- +READ-QUOTED. + ADD 1 TO CSV-POS + PERFORM UNTIL CSV-POS > CSV-LEN + MOVE CSV-TEXT(CSV-POS:1) TO WS-C + IF WS-C = CONST-QUOTE + MOVE "N" TO WS-DOUBLED + IF CSV-POS < CSV-LEN + IF CSV-TEXT(CSV-POS + 1:1) = CONST-QUOTE + MOVE "Y" TO WS-DOUBLED + END-IF + END-IF + IF WS-DOUBLED = "Y" + PERFORM APPEND-CHAR + ADD 2 TO CSV-POS + ELSE + ADD 1 TO CSV-POS + EXIT PERFORM + END-IF + ELSE + PERFORM APPEND-CHAR + ADD 1 TO CSV-POS + END-IF + END-PERFORM + PERFORM UNTIL CSV-POS > CSV-LEN + MOVE CSV-TEXT(CSV-POS:1) TO WS-C + IF WS-C = "," OR WS-C = X"0D" OR WS-C = X"0A" + EXIT PERFORM + END-IF + ADD 1 TO CSV-POS + END-PERFORM. + +*> ------------------------------------------------------------------- +READ-BARE. + PERFORM UNTIL CSV-POS > CSV-LEN + MOVE CSV-TEXT(CSV-POS:1) TO WS-C + IF WS-C = "," OR WS-C = X"0D" OR WS-C = X"0A" + EXIT PERFORM + END-IF + PERFORM APPEND-CHAR + ADD 1 TO CSV-POS + END-PERFORM. + +*> ------------------------------------------------------------------- +CLOSE-FIELD. + IF CSV-POS > CSV-LEN + MOVE "Y" TO CSV-ROW-END + EXIT PARAGRAPH + END-IF + MOVE CSV-TEXT(CSV-POS:1) TO WS-C + EVALUATE TRUE + WHEN WS-C = "," + ADD 1 TO CSV-POS + WHEN WS-C = X"0D" + ADD 1 TO CSV-POS + IF CSV-POS <= CSV-LEN + IF CSV-TEXT(CSV-POS:1) = X"0A" + ADD 1 TO CSV-POS + END-IF + END-IF + MOVE "Y" TO CSV-ROW-END + WHEN WS-C = X"0A" + ADD 1 TO CSV-POS + MOVE "Y" TO CSV-ROW-END + WHEN OTHER + ADD 1 TO CSV-POS + END-EVALUATE. + +*> ------------------------------------------------------------------- +APPEND-CHAR. + IF CSV-FIELD-LEN < 512 + ADD 1 TO CSV-FIELD-LEN + MOVE WS-C TO CSV-FIELD(CSV-FIELD-LEN:1) + END-IF. + +END PROGRAM CEPIS-CSV-NEXT. diff --git a/src/cepis-health.cob b/src/cepis-health.cob new file mode 100644 index 0000000..86da407 --- /dev/null +++ b/src/cepis-health.cob @@ -0,0 +1,93 @@ +IDENTIFICATION DIVISION. +PROGRAM-ID. CEPIS-HEALTH. + +*> --------------------------------------------------------------------- +*> Ask the API how it is. +*> +*> CALL "CEPIS-HEALTH" USING CEPIS-CONFIG CEPIS-HEALTH CEPIS-STATUS +*> +*> Good for two things: confirming the API answers at all, and knowing +*> which version of the database is live. The DNE database is rebuilt +*> every two weeks and HEALTH-DNE identifies the edition in use. +*> +*> An API reporting itself degraded is not a failure of this call. It +*> answers 503 with a body, and that arrives in HEALTH-STATUS with +*> CEPIS-OK set -- checking health is exactly when you want the +*> diagnosis rather than an error. A transport failure still fails. +*> --------------------------------------------------------------------- + +DATA DIVISION. +WORKING-STORAGE SECTION. +COPY "cepis-http.cpy". +COPY "cepis-json.cpy". + +LINKAGE SECTION. +COPY "cepis.cpy". + +PROCEDURE DIVISION USING CEPIS-CONFIG CEPIS-HEALTH CEPIS-STATUS. + +MAIN-START. + INITIALIZE CEPIS-HEALTH + + MOVE SPACES TO HTTPQ-PATH + MOVE "/health" TO HTTPQ-PATH + MOVE 7 TO HTTPQ-PATH-LEN + MOVE "Y" 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 + + IF HTTP-STATUS NOT = 200 AND HTTP-STATUS NOT = 503 + SET CEPIS-INVALID-RESPONSE TO TRUE + MOVE SPACES TO CEPIS-STATUS-MESSAGE + STRING "unexpected status " HTTP-STATUS " from /health" + DELIMITED BY SIZE INTO CEPIS-STATUS-MESSAGE + END-STRING + GOBACK + END-IF + + MOVE "status" TO JS-KEY + PERFORM GET-MEMBER + IF JS-TYPE = "E" + SET CEPIS-INVALID-RESPONSE TO TRUE + MOVE "response to /health is not a JSON object" + TO CEPIS-STATUS-MESSAGE + GOBACK + END-IF + MOVE JS-VALUE TO HEALTH-STATUS + + MOVE "dne" TO JS-KEY + PERFORM GET-MEMBER + MOVE JS-VALUE TO HEALTH-DNE + + MOVE "ceps" TO JS-KEY + PERFORM GET-MEMBER + IF JS-TYPE = "N" + COMPUTE HEALTH-CEPS ROUNDED = FUNCTION NUMVAL (JS-VALUE) + END-IF + + MOVE "geo" TO JS-KEY + PERFORM GET-MEMBER + IF JS-TYPE = "N" + COMPUTE HEALTH-GEO ROUNDED = FUNCTION NUMVAL (JS-VALUE) + END-IF + + MOVE "base_gerada_em" TO JS-KEY + PERFORM GET-MEMBER + MOVE JS-VALUE TO HEALTH-BASE-GERADA-EM + + SET CEPIS-OK TO TRUE + GOBACK. + +*> ------------------------------------------------------------------- +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. + +END PROGRAM CEPIS-HEALTH. diff --git a/src/cepis-http.c b/src/cepis-http.c new file mode 100644 index 0000000..10fa023 --- /dev/null +++ b/src/cepis-http.c @@ -0,0 +1,263 @@ +/* + * Transport primitives for the cep.is COBOL client. + * + * COBOL has no HTTP, and `curl_easy_setopt` is variadic, which a COBOL + * CALL cannot express portably. Everything variadic therefore lives here, + * behind four fixed-signature entry points that a COBOL CALL can reach: + * + * cepis_http_get one GET, no retrying -- the policy stays in COBOL + * cepis_sleep_ms the backoff wait + * cepis_monotonic_ms the clock CFG-TOTAL-TIMEOUT-MS is measured on + * cepis_http_date_delta the RFC 9110 HTTP-date form of Retry-After + * + * All scalar arguments are `int` or `long long` passed BY REFERENCE, which + * COBOL declares as PIC S9(9) COMP-5 and PIC S9(18) COMP-5. All string + * arguments in are NUL-terminated by the caller. All string arguments out + * are blank-filled to their capacity, so the COBOL field they land in is a + * normal space-padded PIC X. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <time.h> + +#include <curl/curl.h> + +#define CEPIS_OK_TRANSPORT 0 +#define CEPIS_ERR_TRANSPORT 1 +#define CEPIS_ERR_TOO_LARGE 2 + +struct cepis_body { + char *buf; + long cap; + long len; + int overflow; +}; + +struct cepis_header { + char *buf; + long cap; + int seen; +}; + +static int cepis_curl_ready; + +static void cepis_blank(char *dst, long cap) +{ + if (dst && cap > 0) { + memset(dst, ' ', (size_t)cap); + } +} + +static void cepis_put(char *dst, long cap, const char *src) +{ + size_t n; + + cepis_blank(dst, cap); + if (!dst || cap <= 0 || !src) { + return; + } + n = strlen(src); + if (n > (size_t)cap) { + n = (size_t)cap; + } + memcpy(dst, src, n); +} + +static size_t cepis_on_body(char *ptr, size_t size, size_t nmemb, void *userdata) +{ + struct cepis_body *sink = (struct cepis_body *)userdata; + size_t n = size * nmemb; + + if (sink->len + (long)n > sink->cap) { + sink->overflow = 1; + return 0; + } + memcpy(sink->buf + sink->len, ptr, n); + sink->len += (long)n; + return n; +} + +static size_t cepis_on_header(char *ptr, size_t size, size_t nmemb, void *userdata) +{ + struct cepis_header *hdr = (struct cepis_header *)userdata; + size_t n = size * nmemb; + static const char name[] = "retry-after:"; + size_t name_len = sizeof(name) - 1; + size_t start, end, len; + + if (hdr->seen || n <= name_len || strncasecmp(ptr, name, name_len) != 0) { + return n; + } + start = name_len; + end = n; + while (start < end && (ptr[start] == ' ' || ptr[start] == '\t')) { + start++; + } + while (end > start && (ptr[end - 1] == '\r' || ptr[end - 1] == '\n' || + ptr[end - 1] == ' ' || ptr[end - 1] == '\t')) { + end--; + } + len = end - start; + if (len > (size_t)hdr->cap) { + len = (size_t)hdr->cap; + } + memcpy(hdr->buf, ptr + start, len); + hdr->seen = 1; + return n; +} + +/* + * Perform one GET. Returns 0 when the exchange completed (whatever the + * status), 1 when it never completed, 2 when the body did not fit. + */ +int cepis_http_get(const char *url, + const char *user_agent, + const int *timeout_ms, + int *http_status, + char *retry_after, + const int *retry_after_cap, + char *body, + const int *body_cap, + int *body_len, + char *error_msg, + const int *error_cap) +{ + CURL *curl; + CURLcode rc; + struct curl_slist *headers = NULL; + struct cepis_body sink; + struct cepis_header hdr; + char errbuf[CURL_ERROR_SIZE]; + long status = 0; + int result = CEPIS_ERR_TRANSPORT; + + *http_status = 0; + *body_len = 0; + cepis_blank(retry_after, *retry_after_cap); + cepis_blank(error_msg, *error_cap); + + if (!cepis_curl_ready) { + if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) { + cepis_put(error_msg, *error_cap, "curl_global_init failed"); + return CEPIS_ERR_TRANSPORT; + } + cepis_curl_ready = 1; + } + + curl = curl_easy_init(); + if (!curl) { + cepis_put(error_msg, *error_cap, "curl_easy_init failed"); + return CEPIS_ERR_TRANSPORT; + } + + sink.buf = body; + sink.cap = *body_cap; + sink.len = 0; + sink.overflow = 0; + + hdr.buf = retry_after; + hdr.cap = *retry_after_cap; + hdr.seen = 0; + + errbuf[0] = '\0'; + + headers = curl_slist_append(headers, + "Accept: application/json, text/csv;q=0.9, */*;q=0.1"); + headers = curl_slist_append(headers, "Accept-Encoding: identity"); + + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L); + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, (long)*timeout_ms); + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, (long)*timeout_ms); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, cepis_on_body); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &sink); + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, cepis_on_header); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &hdr); + curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf); + + rc = curl_easy_perform(curl); + + if (rc == CURLE_OK) { + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status); + *http_status = (int)status; + *body_len = (int)sink.len; + result = CEPIS_OK_TRANSPORT; + } else if (sink.overflow) { + cepis_put(error_msg, *error_cap, "response body exceeds the client buffer"); + result = CEPIS_ERR_TOO_LARGE; + } else { + cepis_put(error_msg, *error_cap, + errbuf[0] ? errbuf : curl_easy_strerror(rc)); + result = CEPIS_ERR_TRANSPORT; + } + + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + return result; +} + +/* Sleep for the backoff. Negative and zero waits return at once. */ +void cepis_sleep_ms(const int *ms) +{ + struct timespec req; + + if (*ms <= 0) { + return; + } + req.tv_sec = *ms / 1000; + req.tv_nsec = (long)(*ms % 1000) * 1000000L; + while (nanosleep(&req, &req) == -1) { + break; + } +} + +/* + * Milliseconds off a monotonic clock. Wall time would let a clock step + * during an NTP correction shorten or extend CFG-TOTAL-TIMEOUT-MS. + */ +void cepis_monotonic_ms(long long *out) +{ + struct timespec now; + + if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) { + *out = 0; + return; + } + *out = (long long)now.tv_sec * 1000LL + (long long)(now.tv_nsec / 1000000L); +} + +/* + * Seconds from now until an HTTP-date, the second form Retry-After may + * take. Returns 0 and sets *seconds when the date parses, 1 otherwise. + * The numeric form is handled in COBOL, where it belongs. + */ +int cepis_http_date_delta(const char *value, int *seconds) +{ + time_t target; + time_t now; + double delta; + + *seconds = 0; + target = curl_getdate(value, NULL); + if (target == (time_t)-1) { + return 1; + } + now = time(NULL); + delta = difftime(target, now); + if (delta < 0) { + delta = 0; + } + if (delta > 2147483000.0) { + delta = 2147483000.0; + } + *seconds = (int)delta; + return 0; +} diff --git a/src/cepis-json.cob b/src/cepis-json.cob new file mode 100644 index 0000000..389d488 --- /dev/null +++ b/src/cepis-json.cob @@ -0,0 +1,409 @@ +IDENTIFICATION DIVISION. +PROGRAM-ID. CEPIS-JSON-GET. + +*> --------------------------------------------------------------------- +*> Read one member out of a JSON object. +*> +*> CALL "CEPIS-JSON-GET" USING JSON-TEXT CEPIS-JSON-SCAN +*> +*> Only the members of the object delimited by JS-FROM and JS-TO are +*> considered; nested objects and arrays are skipped whole, so a member +*> named "lat" inside "geo" is not mistaken for a top-level one. Members +*> the caller does not ask about are ignored, which is what lets a new +*> field appear in the API without breaking this client. +*> +*> String values come back with their escapes decoded, including \uXXXX +*> for the whole of Unicode, encoded as UTF-8. +*> --------------------------------------------------------------------- + +DATA DIVISION. +WORKING-STORAGE SECTION. +01 WS-P PIC 9(9). +01 WS-Q PIC 9(9). +01 WS-C PIC X. +01 WS-D PIC X. +01 WS-DEPTH PIC S9(9). +01 WS-IN-STRING PIC X. +01 WS-LEN PIC 9(9). +01 WS-STR PIC X(1024). +01 WS-STR-LEN PIC 9(9). +01 WS-KEY-TEXT PIC X(32). +01 WS-KEY-LEN PIC 9(9). +01 WS-VAL-START PIC 9(9). +01 WS-VAL-END PIC 9(9). +01 WS-VAL-TYPE PIC X. +01 WS-DONE PIC X. +01 WS-CODE PIC 9(9). +01 WS-LOW PIC 9(9). +01 WS-NIB PIC 9(9). +01 WS-B1 PIC 9(9). +01 WS-B2 PIC 9(9). +01 WS-B3 PIC 9(9). +01 WS-B4 PIC 9(9). +01 WS-REST PIC 9(9). +01 WS-HEX-OK PIC X. +01 WS-I PIC 9(9). +01 CONST-QUOTE PIC X VALUE '"'. +01 CONST-BACKSLASH PIC X VALUE "\". + +LINKAGE SECTION. +01 JSON-TEXT PIC X(262144). +COPY "cepis-json.cpy". + +PROCEDURE DIVISION USING JSON-TEXT CEPIS-JSON-SCAN. + +MAIN-START. + MOVE SPACE TO JS-TYPE + MOVE SPACES TO JS-VALUE + MOVE 0 TO JS-VALUE-LEN + MOVE 0 TO JS-VSTART + MOVE 0 TO JS-VEND + MOVE "N" TO WS-DONE + + MOVE JS-FROM TO WS-P + IF WS-P < 1 + MOVE 1 TO WS-P + END-IF + PERFORM SKIP-BLANKS + IF WS-P > JS-TO + MOVE "E" TO JS-TYPE + GOBACK + END-IF + IF JSON-TEXT(WS-P:1) NOT = "{" + MOVE "E" TO JS-TYPE + GOBACK + END-IF + ADD 1 TO WS-P + + PERFORM UNTIL WS-DONE = "Y" OR WS-P > JS-TO + PERFORM SKIP-BLANKS + IF WS-P > JS-TO + MOVE "Y" TO WS-DONE + EXIT PERFORM + END-IF + IF JSON-TEXT(WS-P:1) = "}" + MOVE "Y" TO WS-DONE + EXIT PERFORM + END-IF + IF JSON-TEXT(WS-P:1) NOT = CONST-QUOTE + MOVE "E" TO JS-TYPE + MOVE "Y" TO WS-DONE + EXIT PERFORM + END-IF + + PERFORM READ-STRING + MOVE WS-STR(1:32) TO WS-KEY-TEXT + MOVE WS-STR-LEN TO WS-KEY-LEN + + PERFORM SKIP-BLANKS + IF WS-P > JS-TO OR JSON-TEXT(WS-P:1) NOT = ":" + MOVE "E" TO JS-TYPE + MOVE "Y" TO WS-DONE + EXIT PERFORM + END-IF + ADD 1 TO WS-P + PERFORM SKIP-BLANKS + + MOVE WS-P TO WS-VAL-START + PERFORM SCAN-VALUE + COMPUTE WS-VAL-END = WS-P - 1 + + IF WS-KEY-TEXT = JS-KEY AND WS-KEY-LEN <= 32 + PERFORM TAKE-VALUE + MOVE "Y" TO WS-DONE + EXIT PERFORM + END-IF + + PERFORM SKIP-BLANKS + IF WS-P > JS-TO + MOVE "Y" TO WS-DONE + ELSE + IF JSON-TEXT(WS-P:1) = "," + ADD 1 TO WS-P + ELSE + MOVE "Y" TO WS-DONE + END-IF + END-IF + END-PERFORM + GOBACK. + +*> ------------------------------------------------------------------- +SKIP-BLANKS. + PERFORM UNTIL WS-P > JS-TO + MOVE JSON-TEXT(WS-P:1) TO WS-C + IF WS-C = SPACE OR WS-C = X"09" OR WS-C = X"0A" OR WS-C = X"0D" + ADD 1 TO WS-P + ELSE + EXIT PERFORM + END-IF + END-PERFORM. + +*> ------------------------------------------------------------------- +TAKE-VALUE. + MOVE WS-VAL-TYPE TO JS-TYPE + MOVE WS-VAL-START TO JS-VSTART + MOVE WS-VAL-END TO JS-VEND + IF WS-VAL-TYPE = "S" + MOVE WS-VAL-START TO WS-P + PERFORM READ-STRING + MOVE WS-STR TO JS-VALUE + MOVE WS-STR-LEN TO JS-VALUE-LEN + ELSE + COMPUTE WS-LEN = WS-VAL-END - WS-VAL-START + 1 + IF WS-LEN > 1024 + MOVE 1024 TO WS-LEN + END-IF + IF WS-LEN > 0 + MOVE JSON-TEXT(WS-VAL-START:WS-LEN) TO JS-VALUE + MOVE WS-LEN TO JS-VALUE-LEN + END-IF + END-IF. + +*> ------------------------------------------------------------------- +*> Classify the value at WS-P and step past it. Objects and arrays are +*> walked with a depth counter that is blind inside string literals, so +*> a brace in a street name cannot close the object early. +SCAN-VALUE. + IF WS-P > JS-TO + MOVE "E" TO WS-VAL-TYPE + EXIT PARAGRAPH + END-IF + MOVE JSON-TEXT(WS-P:1) TO WS-C + EVALUATE TRUE + WHEN WS-C = CONST-QUOTE + MOVE "S" TO WS-VAL-TYPE + PERFORM SKIP-STRING + WHEN WS-C = "{" + MOVE "O" TO WS-VAL-TYPE + PERFORM SKIP-NESTED + WHEN WS-C = "[" + MOVE "A" TO WS-VAL-TYPE + PERFORM SKIP-NESTED + WHEN WS-C = "t" OR WS-C = "f" + MOVE "B" TO WS-VAL-TYPE + PERFORM SKIP-BARE + WHEN WS-C = "n" + MOVE "Z" TO WS-VAL-TYPE + PERFORM SKIP-BARE + WHEN OTHER + MOVE "N" TO WS-VAL-TYPE + PERFORM SKIP-BARE + END-EVALUATE. + +*> ------------------------------------------------------------------- +SKIP-BARE. + PERFORM UNTIL WS-P > JS-TO + MOVE JSON-TEXT(WS-P:1) TO WS-C + IF WS-C = "," OR WS-C = "}" OR WS-C = "]" + OR WS-C = SPACE OR WS-C = X"09" OR WS-C = X"0A" OR WS-C = X"0D" + EXIT PERFORM + END-IF + ADD 1 TO WS-P + END-PERFORM. + +*> ------------------------------------------------------------------- +SKIP-STRING. + ADD 1 TO WS-P + PERFORM UNTIL WS-P > JS-TO + MOVE JSON-TEXT(WS-P:1) TO WS-C + IF WS-C = CONST-BACKSLASH + ADD 2 TO WS-P + ELSE + ADD 1 TO WS-P + IF WS-C = CONST-QUOTE + EXIT PERFORM + END-IF + END-IF + END-PERFORM. + +*> ------------------------------------------------------------------- +SKIP-NESTED. + MOVE 0 TO WS-DEPTH + MOVE "N" TO WS-IN-STRING + PERFORM UNTIL WS-P > JS-TO + MOVE JSON-TEXT(WS-P:1) TO WS-C + IF WS-IN-STRING = "Y" + IF WS-C = CONST-BACKSLASH + ADD 2 TO WS-P + ELSE + IF WS-C = CONST-QUOTE + MOVE "N" TO WS-IN-STRING + END-IF + ADD 1 TO WS-P + END-IF + ELSE + EVALUATE TRUE + WHEN WS-C = CONST-QUOTE + MOVE "Y" TO WS-IN-STRING + ADD 1 TO WS-P + WHEN WS-C = "{" OR WS-C = "[" + ADD 1 TO WS-DEPTH + ADD 1 TO WS-P + WHEN WS-C = "}" OR WS-C = "]" + SUBTRACT 1 FROM WS-DEPTH + ADD 1 TO WS-P + WHEN OTHER + ADD 1 TO WS-P + END-EVALUATE + IF WS-DEPTH <= 0 + EXIT PERFORM + END-IF + END-IF + END-PERFORM. + +*> ------------------------------------------------------------------- +*> Read the string literal starting at WS-P into WS-STR, decoding the +*> escapes, and leave WS-P just past the closing quote. +READ-STRING. + MOVE SPACES TO WS-STR + MOVE 0 TO WS-STR-LEN + ADD 1 TO WS-P + PERFORM UNTIL WS-P > JS-TO + MOVE JSON-TEXT(WS-P:1) TO WS-C + IF WS-C = CONST-QUOTE + ADD 1 TO WS-P + EXIT PERFORM + END-IF + IF WS-C = CONST-BACKSLASH + ADD 1 TO WS-P + IF WS-P > JS-TO + EXIT PERFORM + END-IF + MOVE JSON-TEXT(WS-P:1) TO WS-D + EVALUATE WS-D + WHEN "n" + MOVE X"0A" TO WS-C + PERFORM APPEND-CHAR + ADD 1 TO WS-P + WHEN "t" + MOVE X"09" TO WS-C + PERFORM APPEND-CHAR + ADD 1 TO WS-P + WHEN "r" + MOVE X"0D" TO WS-C + PERFORM APPEND-CHAR + ADD 1 TO WS-P + WHEN "b" + MOVE X"08" TO WS-C + PERFORM APPEND-CHAR + ADD 1 TO WS-P + WHEN "f" + MOVE X"0C" TO WS-C + PERFORM APPEND-CHAR + ADD 1 TO WS-P + WHEN "u" + PERFORM APPEND-UNICODE + WHEN OTHER + MOVE WS-D TO WS-C + PERFORM APPEND-CHAR + ADD 1 TO WS-P + END-EVALUATE + ELSE + PERFORM APPEND-CHAR + ADD 1 TO WS-P + END-IF + END-PERFORM. + +*> ------------------------------------------------------------------- +APPEND-CHAR. + IF WS-STR-LEN < 1024 + ADD 1 TO WS-STR-LEN + MOVE WS-C TO WS-STR(WS-STR-LEN:1) + END-IF. + +*> ------------------------------------------------------------------- +*> WS-P sits on the "u" of a \uXXXX escape. Decode it, join it with the +*> escape that follows when the two form a surrogate pair, and append the +*> UTF-8 bytes. +APPEND-UNICODE. + ADD 1 TO WS-P + MOVE 0 TO WS-CODE + PERFORM READ-HEX-4 + IF WS-HEX-OK NOT = "Y" + EXIT PARAGRAPH + END-IF + IF WS-CODE >= 55296 AND WS-CODE <= 56319 + IF WS-P + 5 <= JS-TO + AND JSON-TEXT(WS-P:1) = CONST-BACKSLASH + AND JSON-TEXT(WS-P + 1:1) = "u" + MOVE WS-CODE TO WS-LOW + ADD 2 TO WS-P + MOVE 0 TO WS-CODE + PERFORM READ-HEX-4 + IF WS-HEX-OK = "Y" AND WS-CODE >= 56320 AND WS-CODE <= 57343 + COMPUTE WS-CODE = + 65536 + (WS-LOW - 55296) * 1024 + (WS-CODE - 56320) + END-IF + END-IF + END-IF + PERFORM EMIT-UTF8. + +*> ------------------------------------------------------------------- +READ-HEX-4. + MOVE "Y" TO WS-HEX-OK + PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > 4 + IF WS-P > JS-TO + MOVE "N" TO WS-HEX-OK + EXIT PERFORM + END-IF + MOVE JSON-TEXT(WS-P:1) TO WS-D + EVALUATE TRUE + WHEN WS-D >= "0" AND WS-D <= "9" + COMPUTE WS-NIB = FUNCTION ORD(WS-D) - FUNCTION ORD("0") + WHEN WS-D >= "a" AND WS-D <= "f" + COMPUTE WS-NIB = FUNCTION ORD(WS-D) - FUNCTION ORD("a") + 10 + WHEN WS-D >= "A" AND WS-D <= "F" + COMPUTE WS-NIB = FUNCTION ORD(WS-D) - FUNCTION ORD("A") + 10 + WHEN OTHER + MOVE "N" TO WS-HEX-OK + EXIT PERFORM + END-EVALUATE + COMPUTE WS-CODE = WS-CODE * 16 + WS-NIB + ADD 1 TO WS-P + END-PERFORM. + +*> ------------------------------------------------------------------- +EMIT-UTF8. + EVALUATE TRUE + WHEN WS-CODE < 128 + MOVE FUNCTION CHAR(WS-CODE + 1) TO WS-C + PERFORM APPEND-CHAR + WHEN WS-CODE < 2048 + DIVIDE WS-CODE BY 64 GIVING WS-B1 REMAINDER WS-B2 + COMPUTE WS-B1 = 192 + WS-B1 + COMPUTE WS-B2 = 128 + WS-B2 + MOVE FUNCTION CHAR(WS-B1 + 1) TO WS-C + PERFORM APPEND-CHAR + MOVE FUNCTION CHAR(WS-B2 + 1) TO WS-C + PERFORM APPEND-CHAR + WHEN WS-CODE < 65536 + DIVIDE WS-CODE BY 4096 GIVING WS-B1 REMAINDER WS-REST + DIVIDE WS-REST BY 64 GIVING WS-B2 REMAINDER WS-B3 + COMPUTE WS-B1 = 224 + WS-B1 + COMPUTE WS-B2 = 128 + WS-B2 + COMPUTE WS-B3 = 128 + WS-B3 + MOVE FUNCTION CHAR(WS-B1 + 1) TO WS-C + PERFORM APPEND-CHAR + MOVE FUNCTION CHAR(WS-B2 + 1) TO WS-C + PERFORM APPEND-CHAR + MOVE FUNCTION CHAR(WS-B3 + 1) TO WS-C + PERFORM APPEND-CHAR + WHEN OTHER + DIVIDE WS-CODE BY 262144 GIVING WS-B1 REMAINDER WS-REST + DIVIDE WS-REST BY 4096 GIVING WS-B2 REMAINDER WS-Q + DIVIDE WS-Q BY 64 GIVING WS-B3 REMAINDER WS-B4 + COMPUTE WS-B1 = 240 + WS-B1 + COMPUTE WS-B2 = 128 + WS-B2 + COMPUTE WS-B3 = 128 + WS-B3 + COMPUTE WS-B4 = 128 + WS-B4 + MOVE FUNCTION CHAR(WS-B1 + 1) TO WS-C + PERFORM APPEND-CHAR + MOVE FUNCTION CHAR(WS-B2 + 1) TO WS-C + PERFORM APPEND-CHAR + MOVE FUNCTION CHAR(WS-B3 + 1) TO WS-C + PERFORM APPEND-CHAR + MOVE FUNCTION CHAR(WS-B4 + 1) TO WS-C + PERFORM APPEND-CHAR + END-EVALUATE. + +END PROGRAM CEPIS-JSON-GET. diff --git a/src/cepis-lookup-many.cob b/src/cepis-lookup-many.cob new file mode 100644 index 0000000..d76fa72 --- /dev/null +++ b/src/cepis-lookup-many.cob @@ -0,0 +1,346 @@ +IDENTIFICATION DIVISION. +PROGRAM-ID. CEPIS-LOOKUP-MANY. + +*> --------------------------------------------------------------------- +*> Resolve a whole table of CEPs. +*> +*> CALL "CEPIS-LOOKUP-MANY" +*> USING CEPIS-CONFIG CEPIS-BATCH CEPIS-STATUS +*> +*> Fill BATCH-COUNT and BATCH-REQUESTED for each slot; every slot comes +*> back in place. A CEP that does not exist, or that is not eight +*> digits, leaves its slot BATCH-ENTRY-MISSING -- it is never removed, +*> never shifted, and never raises. In a batch of a thousand, one bad +*> CEP must not cost the other nine hundred and ninety-nine. To find out +*> which one was malformed, call CEPIS-NORMALIZE-CEP on it first. +*> +*> Do not loop over CEPIS-LOOKUP instead. This resolves up to 500 CEPs +*> per request and chunks longer tables automatically; a loop of 500 +*> calls pays 500 TLS handshakes where the batch pays one. +*> +*> The batch carries no coordinate. The endpoint behind it is CSV and +*> the CSV has no geo column, so BAT-GEO-PRESENT is "N" on every slot, +*> including CEPs that do have a coordinate. Need it? Use CEPIS-LOOKUP. +*> +*> The table holds 1000 slots. A BATCH-COUNT above that is rejected with +*> CEPIS-INVALID-CEP rather than silently truncated. +*> --------------------------------------------------------------------- + +DATA DIVISION. +WORKING-STORAGE SECTION. +COPY "cepis-http.cpy". +COPY "cepis-csv.cpy". + +01 WS-NORM. + COPY "cepis-status.cpy" REPLACING + ==:P:== BY ==WSN== ==:L1:== BY ==05==. + +01 WS-CHUNK-MAX PIC 9(4) VALUE 500. +01 WS-BATCH-MAX PIC 9(4) VALUE 1000. +01 WS-COLUMNS PIC 9(4) VALUE 13. + +01 WS-INPUT PIC X(20). +01 WS-CEP PIC X(8). +01 WS-ROW-CEP PIC X(8). +01 WS-NEXT PIC 9(4). +01 WS-CHUNK-N PIC 9(4). +01 WS-ROW PIC 9(4). +01 WS-SLOTX PIC 9(4). +01 WS-I PIC 9(4). +01 WS-K PIC 9(4). +01 WS-ID PIC 9(4). +01 WS-NCOLS PIC 9(4). +01 WS-QLEN PIC 9(9). +01 WS-QUERY PIC X(5000). + +01 WS-CHUNK-MAP. + 05 WS-CHUNK-ROW OCCURS 500 TIMES. + 10 WS-SLOT PIC 9(4). + 10 WS-SENT PIC X(8). + +01 WS-HEADER-MAP. + 05 WS-COL-ID OCCURS 64 TIMES PIC 9(4). + +01 WS-ROW-VALUES. + 05 WS-ROW-FIELD OCCURS 13 TIMES. + 10 WS-VAL PIC X(512). + 10 WS-VAL-LEN PIC 9(9). + +01 WS-NAME-TABLE. + 05 FILLER PIC X(16) VALUE "cep". + 05 FILLER PIC X(16) VALUE "logradouro". + 05 FILLER PIC X(16) VALUE "complemento". + 05 FILLER PIC X(16) VALUE "unidade". + 05 FILLER PIC X(16) VALUE "bairro". + 05 FILLER PIC X(16) VALUE "localidade". + 05 FILLER PIC X(16) VALUE "uf". + 05 FILLER PIC X(16) VALUE "estado". + 05 FILLER PIC X(16) VALUE "regiao". + 05 FILLER PIC X(16) VALUE "ibge". + 05 FILLER PIC X(16) VALUE "gia". + 05 FILLER PIC X(16) VALUE "ddd". + 05 FILLER PIC X(16) VALUE "siafi". +01 WS-NAMES REDEFINES WS-NAME-TABLE. + 05 WS-NAME OCCURS 13 TIMES PIC X(16). + +LINKAGE SECTION. +COPY "cepis.cpy". + +PROCEDURE DIVISION USING CEPIS-CONFIG CEPIS-BATCH CEPIS-STATUS. + +MAIN-START. + SET CEPIS-OK TO TRUE + MOVE SPACES TO CEPIS-STATUS-MESSAGE + MOVE 0 TO CEPIS-STATUS-HTTP + MOVE -1 TO CEPIS-STATUS-RETRY-AFTER-MS + MOVE 0 TO CEPIS-STATUS-ATTEMPTS + + IF BATCH-COUNT = 0 + GOBACK + END-IF + IF BATCH-COUNT > WS-BATCH-MAX + SET CEPIS-INVALID-CEP TO TRUE + MOVE SPACES TO CEPIS-STATUS-MESSAGE + STRING "BATCH-COUNT is " BATCH-COUNT + " but the table holds only " WS-BATCH-MAX " slots" + DELIMITED BY SIZE INTO CEPIS-STATUS-MESSAGE + END-STRING + GOBACK + END-IF + + PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > BATCH-COUNT + PERFORM CLEAR-SLOT + END-PERFORM + + MOVE 1 TO WS-NEXT + PERFORM UNTIL WS-NEXT > BATCH-COUNT + PERFORM BUILD-CHUNK + IF WS-CHUNK-N > 0 + PERFORM FETCH-CHUNK + IF CEPIS-FAILED + GOBACK + END-IF + PERFORM PARSE-CHUNK + IF CEPIS-FAILED + GOBACK + END-IF + END-IF + END-PERFORM + GOBACK. + +*> ------------------------------------------------------------------- +CLEAR-SLOT. + INITIALIZE BATCH-ADDRESS (WS-I) + SET BATCH-ENTRY-MISSING (WS-I) TO TRUE + SET BAT-NOT-TRUNCATED (WS-I) TO TRUE + SET BAT-GEO-ABSENT (WS-I) TO TRUE + MOVE SPACES TO BAT-GEO-PRECISAO (WS-I). + +*> ------------------------------------------------------------------- +*> Collect the next run of at most 500 well-formed CEPs and remember, +*> for each, which slot it came from. Malformed entries are skipped +*> here: they keep their slot but cost no round trip, because a CEP that +*> is not eight digits is the caller's bug and the server can only say +*> so more slowly. +BUILD-CHUNK. + MOVE 0 TO WS-CHUNK-N + MOVE 0 TO WS-QLEN + MOVE SPACES TO WS-QUERY + PERFORM UNTIL WS-NEXT > BATCH-COUNT + OR WS-CHUNK-N >= WS-CHUNK-MAX + MOVE BATCH-REQUESTED (WS-NEXT) TO WS-INPUT + CALL "CEPIS-NORMALIZE-CEP" USING WS-INPUT WS-CEP WS-NORM + END-CALL + IF WSN-OK + ADD 1 TO WS-CHUNK-N + MOVE WS-NEXT TO WS-SLOT (WS-CHUNK-N) + MOVE WS-CEP TO WS-SENT (WS-CHUNK-N) + IF WS-QLEN > 0 + ADD 1 TO WS-QLEN + MOVE "," TO WS-QUERY (WS-QLEN:1) + END-IF + MOVE WS-CEP TO WS-QUERY (WS-QLEN + 1:8) + ADD 8 TO WS-QLEN + END-IF + ADD 1 TO WS-NEXT + END-PERFORM. + +*> ------------------------------------------------------------------- +FETCH-CHUNK. + MOVE SPACES TO HTTPQ-PATH + STRING "/csv?ceps=" WS-QUERY (1:WS-QLEN) + DELIMITED BY SIZE INTO HTTPQ-PATH + END-STRING + COMPUTE HTTPQ-PATH-LEN = 10 + WS-QLEN + 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 + EXIT PARAGRAPH + END-IF + + IF HTTP-STATUS = 400 + SET CEPIS-INVALID-CEP TO TRUE + MOVE "the API rejected the batch request" TO + CEPIS-STATUS-MESSAGE + EXIT PARAGRAPH + END-IF + IF HTTP-STATUS NOT = 200 + SET CEPIS-INVALID-RESPONSE TO TRUE + MOVE SPACES TO CEPIS-STATUS-MESSAGE + STRING "unexpected status " HTTP-STATUS " from /csv" + DELIMITED BY SIZE INTO CEPIS-STATUS-MESSAGE + END-STRING + END-IF. + +*> ------------------------------------------------------------------- +*> The API answers one row per requested CEP, in the requested order. +*> Should that ever fail to hold -- a proxy truncating, a cut response +*> -- the rows that did arrive are placed and the rest of the slots stay +*> missing. Padding beats shifting: a shifted list silently pairs the +*> wrong address with the wrong CEP, and nothing downstream can tell. +PARSE-CHUNK. + MOVE 1 TO CSV-POS + MOVE HTTP-BODY-LEN TO CSV-LEN + IF CSV-LEN >= 3 + IF HTTP-BODY (1:3) = X"EFBBBF" + MOVE 4 TO CSV-POS + END-IF + END-IF + + PERFORM READ-HEADER + IF WS-NCOLS = 0 + SET CEPIS-INVALID-RESPONSE TO TRUE + MOVE "the batch response carries no CSV header row" + TO CEPIS-STATUS-MESSAGE + EXIT PARAGRAPH + END-IF + + MOVE 0 TO WS-ROW + PERFORM UNTIL CSV-POS > CSV-LEN OR WS-ROW >= WS-CHUNK-N + ADD 1 TO WS-ROW + PERFORM READ-ROW + PERFORM ASSIGN-ROW + END-PERFORM. + +*> ------------------------------------------------------------------- +*> Map the header's column order onto the field identifiers, so a column +*> the API adds, moves or drops cannot shift the others out of place. +READ-HEADER. + MOVE 0 TO WS-NCOLS + PERFORM VARYING WS-K FROM 1 BY 1 UNTIL WS-K > 64 + MOVE 0 TO WS-COL-ID (WS-K) + END-PERFORM + MOVE 0 TO WS-K + PERFORM UNTIL WS-K >= 64 + ADD 1 TO WS-K + CALL "CEPIS-CSV-NEXT" USING HTTP-BODY CEPIS-CSV-SCAN + END-CALL + IF CSV-EXHAUSTED = "Y" + SUBTRACT 1 FROM WS-K + EXIT PERFORM + END-IF + MOVE WS-K TO WS-NCOLS + PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > WS-COLUMNS + IF CSV-FIELD (1:16) = WS-NAME (WS-I) + MOVE WS-I TO WS-COL-ID (WS-K) + EXIT PERFORM + END-IF + END-PERFORM + IF CSV-ROW-END = "Y" + EXIT PERFORM + END-IF + END-PERFORM. + +*> ------------------------------------------------------------------- +READ-ROW. + PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > WS-COLUMNS + MOVE SPACES TO WS-VAL (WS-I) + MOVE 0 TO WS-VAL-LEN (WS-I) + END-PERFORM + MOVE 0 TO WS-K + PERFORM UNTIL WS-K >= 64 + ADD 1 TO WS-K + CALL "CEPIS-CSV-NEXT" USING HTTP-BODY CEPIS-CSV-SCAN + END-CALL + IF WS-K <= WS-NCOLS + MOVE WS-COL-ID (WS-K) TO WS-ID + IF WS-ID > 0 + MOVE CSV-FIELD TO WS-VAL (WS-ID) + MOVE CSV-FIELD-LEN TO WS-VAL-LEN (WS-ID) + END-IF + END-IF + IF CSV-ROW-END = "Y" + EXIT PERFORM + END-IF + END-PERFORM. + +*> ------------------------------------------------------------------- +*> A row the API filled only in its cep column means "not in the DNE +*> database"; it keeps its slot as missing. The cep column is also +*> matched against the CEP that was actually sent, so a response that +*> came back in the wrong order marks slots missing instead of pairing +*> them with someone else's street. +ASSIGN-ROW. + MOVE WS-SLOT (WS-ROW) TO WS-SLOTX + IF WS-VAL (6) = SPACES + EXIT PARAGRAPH + END-IF + MOVE WS-VAL (1) (1:20) TO WS-INPUT + CALL "CEPIS-NORMALIZE-CEP" USING WS-INPUT WS-ROW-CEP WS-NORM + END-CALL + IF NOT WSN-OK OR WS-ROW-CEP NOT = WS-SENT (WS-ROW) + EXIT PARAGRAPH + END-IF + + SET BATCH-ENTRY-FOUND (WS-SLOTX) TO TRUE + MOVE WS-ROW-CEP TO BAT-CEP (WS-SLOTX) + STRING WS-ROW-CEP (1:5) "-" WS-ROW-CEP (6:3) + DELIMITED BY SIZE INTO BAT-CEP-MASKED (WS-SLOTX) + END-STRING + + IF WS-VAL-LEN (2) > 120 + SET BAT-WAS-TRUNCATED (WS-SLOTX) TO TRUE + END-IF + MOVE WS-VAL (2) TO BAT-LOGRADOURO (WS-SLOTX) + + IF WS-VAL-LEN (3) > 120 + SET BAT-WAS-TRUNCATED (WS-SLOTX) TO TRUE + END-IF + MOVE WS-VAL (3) TO BAT-COMPLEMENTO (WS-SLOTX) + + IF WS-VAL-LEN (4) > 80 + SET BAT-WAS-TRUNCATED (WS-SLOTX) TO TRUE + END-IF + MOVE WS-VAL (4) TO BAT-UNIDADE (WS-SLOTX) + + IF WS-VAL-LEN (5) > 100 + SET BAT-WAS-TRUNCATED (WS-SLOTX) TO TRUE + END-IF + MOVE WS-VAL (5) TO BAT-BAIRRO (WS-SLOTX) + + IF WS-VAL-LEN (6) > 100 + SET BAT-WAS-TRUNCATED (WS-SLOTX) TO TRUE + END-IF + MOVE WS-VAL (6) TO BAT-LOCALIDADE (WS-SLOTX) + + MOVE WS-VAL (7) TO BAT-UF (WS-SLOTX) + + IF WS-VAL-LEN (8) > 60 + SET BAT-WAS-TRUNCATED (WS-SLOTX) TO TRUE + END-IF + MOVE WS-VAL (8) TO BAT-ESTADO (WS-SLOTX) + + IF WS-VAL-LEN (9) > 30 + SET BAT-WAS-TRUNCATED (WS-SLOTX) TO TRUE + END-IF + MOVE WS-VAL (9) TO BAT-REGIAO (WS-SLOTX) + + MOVE WS-VAL (10) TO BAT-IBGE (WS-SLOTX) + MOVE WS-VAL (11) TO BAT-GIA (WS-SLOTX) + MOVE WS-VAL (12) TO BAT-DDD (WS-SLOTX) + MOVE WS-VAL (13) TO BAT-SIAFI (WS-SLOTX). + +END PROGRAM CEPIS-LOOKUP-MANY. diff --git a/src/cepis-lookup.cob b/src/cepis-lookup.cob new file mode 100644 index 0000000..cc6e412 --- /dev/null +++ b/src/cepis-lookup.cob @@ -0,0 +1,324 @@ +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. +*> --------------------------------------------------------------------- + +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-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 + 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. + +*> ------------------------------------------------------------------- +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. + +*> ------------------------------------------------------------------- +*> 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. diff --git a/src/cepis-normalize-cep.cob b/src/cepis-normalize-cep.cob new file mode 100644 index 0000000..41615ad --- /dev/null +++ b/src/cepis-normalize-cep.cob @@ -0,0 +1,74 @@ +IDENTIFICATION DIVISION. +PROGRAM-ID. CEPIS-NORMALIZE-CEP. + +*> --------------------------------------------------------------------- +*> Reduce a CEP to its eight digits. +*> +*> CALL "CEPIS-NORMALIZE-CEP" +*> USING CEPIS-CEP-INPUT CEPIS-CEP CEPIS-STATUS +*> +*> "01001-000", "01001000", " 01001-000 " and "01001.000" all give +*> "01001000". Anything that does not leave exactly eight digits sets +*> CEPIS-INVALID-CEP and leaves CEPIS-CEP as spaces. +*> +*> CEPIS-LOOKUP runs this before opening a socket. A seven-digit CEP is +*> the caller's bug, and a round trip for the server to say so wastes +*> both sides' time. +*> --------------------------------------------------------------------- + +DATA DIVISION. +WORKING-STORAGE SECTION. +01 WS-I PIC 9(4). +01 WS-N PIC 9(4). +01 WS-C PIC X. +01 WS-CLEAN PIC X(20). + +LINKAGE SECTION. +COPY "cepis.cpy". + +PROCEDURE DIVISION USING CEPIS-CEP-INPUT CEPIS-CEP CEPIS-STATUS. + +MAIN-START. + MOVE SPACES TO CEPIS-CEP + MOVE SPACES TO WS-CLEAN + MOVE 0 TO WS-N + MOVE 0 TO CEPIS-STATUS-HTTP + MOVE 0 TO CEPIS-STATUS-ATTEMPTS + MOVE -1 TO CEPIS-STATUS-RETRY-AFTER-MS + MOVE SPACES TO CEPIS-STATUS-MESSAGE + + PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > 20 + MOVE CEPIS-CEP-INPUT(WS-I:1) TO WS-C + EVALUATE TRUE + WHEN WS-C = SPACE OR WS-C = "-" OR WS-C = "." + CONTINUE + WHEN WS-C >= "0" AND WS-C <= "9" + ADD 1 TO WS-N + IF WS-N <= 20 + MOVE WS-C TO WS-CLEAN(WS-N:1) + END-IF + WHEN OTHER + PERFORM REJECT + GOBACK + END-EVALUATE + END-PERFORM + + IF WS-N NOT = 8 + PERFORM REJECT + GOBACK + END-IF + + MOVE WS-CLEAN(1:8) TO CEPIS-CEP + SET CEPIS-OK TO TRUE + GOBACK. + +*> ------------------------------------------------------------------- +REJECT. + SET CEPIS-INVALID-CEP TO TRUE + MOVE SPACES TO CEPIS-STATUS-MESSAGE + STRING "CEP must be 8 digits, got '" + FUNCTION TRIM(CEPIS-CEP-INPUT) "'" + DELIMITED BY SIZE INTO CEPIS-STATUS-MESSAGE + END-STRING. + +END PROGRAM CEPIS-NORMALIZE-CEP. diff --git a/src/cepis-request.cob b/src/cepis-request.cob new file mode 100644 index 0000000..b45f5da --- /dev/null +++ b/src/cepis-request.cob @@ -0,0 +1,363 @@ +IDENTIFICATION DIVISION. +PROGRAM-ID. CEPIS-REQUEST. + +*> --------------------------------------------------------------------- +*> One GET against the API, retried according to CEPIS-CONFIG. Internal: +*> callers of this library use CEPIS-LOOKUP and friends. +*> +*> CALL "CEPIS-REQUEST" USING CEPIS-CONFIG CEPIS-HTTP-REQUEST +*> CEPIS-HTTP-RESPONSE CEPIS-STATUS +*> +*> 429 and the transient 5xx are retried, and so are transport failures. +*> 400 and 404 are handed back to the caller untouched: both are +*> definitive answers about the request, so retrying returns the same +*> thing more slowly while burning someone else's quota. +*> +*> Waits are drawn from [0, min(base * 2^(n-1), max)] -- full jitter. The +*> randomness is not decoration: without it, N clients that failed +*> together come back together and reproduce the spike. +*> +*> A Retry-After from the server wins over that calculation, in both the +*> RFC 9110 forms, and is still capped at CFG-BACKOFF-MAX-MS so that a +*> misconfigured proxy answering "Retry-After: 3600" cannot park the +*> caller for an hour. +*> --------------------------------------------------------------------- + +DATA DIVISION. +WORKING-STORAGE SECTION. +01 WS-SEEDED PIC X VALUE "N". +01 WS-SEED PIC 9(9). +01 WS-RANDOM PIC 9V9(9). + +01 WS-BASE PIC X(200). +01 WS-BASE-LEN PIC S9(9). +01 WS-URL PIC X(8500). +01 WS-URL-TEXT PIC X(8500). +01 WS-USER-AGENT PIC X(140). + +01 WS-TIMEOUT-MS PIC S9(9) COMP-5. +01 WS-CURL-STATUS PIC S9(9) COMP-5. +01 WS-BODY-LEN PIC S9(9) COMP-5. +01 WS-BODY-CAP PIC S9(9) COMP-5 VALUE 262144. +01 WS-RETRY-AFTER-CAP PIC S9(9) COMP-5 VALUE 128. +01 WS-ERROR-CAP PIC S9(9) COMP-5 VALUE 256. +01 WS-SECONDS PIC S9(9) COMP-5. +01 WS-SLEEP-MS PIC S9(9) COMP-5. +01 WS-RC PIC S9(9) COMP-5. +01 WS-DATE-RC PIC S9(9) COMP-5. + +01 WS-RETRY-AFTER-TEXT PIC X(128). +01 WS-RETRY-AFTER-TRIM PIC X(128). +01 WS-RETRY-AFTER-Z PIC X(129). +01 WS-ERROR-TEXT PIC X(256). + +01 WS-NOW PIC S9(18) COMP-5. +01 WS-DEADLINE PIC S9(18) COMP-5. +01 WS-REMAINING PIC S9(18). +01 WS-HAS-DEADLINE PIC X. + +01 WS-MAX-ATTEMPTS PIC 9(4). +01 WS-ATTEMPT PIC 9(4). +01 WS-EXPONENT PIC 9(4). +01 WS-CEILING PIC 9(18). +01 WS-WAIT-MS PIC 9(18). +01 WS-RETRY-AFTER-MS PIC S9(18). +01 WS-LAST-STATUS PIC 9(3). +01 WS-DONE PIC X. +01 WS-KEEP PIC X. +01 WS-DIGITS PIC 9(4). +01 WS-JUNK PIC 9(4). +01 WS-I PIC 9(4). +01 WS-C PIC X. +01 WS-TRIM-LEN PIC 9(4). + +LINKAGE SECTION. +COPY "cepis.cpy". +COPY "cepis-http.cpy". + +PROCEDURE DIVISION USING CEPIS-CONFIG CEPIS-HTTP-REQUEST + CEPIS-HTTP-RESPONSE CEPIS-STATUS. + +MAIN-START. + PERFORM SEED-JITTER + PERFORM RESET-STATE + PERFORM BUILD-URL + PERFORM ARM-DEADLINE + + PERFORM VARYING WS-ATTEMPT FROM 1 BY 1 + UNTIL WS-ATTEMPT > WS-MAX-ATTEMPTS + + IF WS-HAS-DEADLINE = "Y" + CALL "cepis_monotonic_ms" USING WS-NOW + IF WS-NOW >= WS-DEADLINE + EXIT PERFORM + END-IF + END-IF + + MOVE WS-ATTEMPT TO CEPIS-STATUS-ATTEMPTS + PERFORM ONE-ATTEMPT + + IF WS-DONE = "Y" + EXIT PERFORM + END-IF + IF WS-ATTEMPT >= WS-MAX-ATTEMPTS + EXIT PERFORM + END-IF + + PERFORM COMPUTE-WAIT + IF WS-HAS-DEADLINE = "Y" + CALL "cepis_monotonic_ms" USING WS-NOW + COMPUTE WS-REMAINING = WS-DEADLINE - WS-NOW + IF WS-REMAINING <= 0 + EXIT PERFORM + END-IF + IF WS-WAIT-MS > WS-REMAINING + MOVE WS-REMAINING TO WS-WAIT-MS + END-IF + END-IF + MOVE WS-WAIT-MS TO WS-SLEEP-MS + CALL "cepis_sleep_ms" USING WS-SLEEP-MS + END-PERFORM + + IF WS-DONE NOT = "Y" + PERFORM CLASSIFY-FAILURE + END-IF + GOBACK. + +*> ------------------------------------------------------------------- +*> The jitter is seeded once per process, off the monotonic clock. An +*> unseeded FUNCTION RANDOM gives every process the same sequence, which +*> is the one thing full jitter exists to avoid. +SEED-JITTER. + IF WS-SEEDED = "N" + CALL "cepis_monotonic_ms" USING WS-NOW + COMPUTE WS-SEED = FUNCTION MOD (WS-NOW, 1000000) + COMPUTE WS-RANDOM = FUNCTION RANDOM (WS-SEED) + MOVE "Y" TO WS-SEEDED + END-IF. + +*> ------------------------------------------------------------------- +RESET-STATE. + SET CEPIS-OK TO TRUE + MOVE SPACES TO CEPIS-STATUS-MESSAGE + MOVE 0 TO CEPIS-STATUS-HTTP + MOVE -1 TO CEPIS-STATUS-RETRY-AFTER-MS + MOVE 0 TO CEPIS-STATUS-ATTEMPTS + MOVE 0 TO HTTP-STATUS + MOVE 0 TO HTTP-BODY-LEN + MOVE "N" TO WS-DONE + MOVE 0 TO WS-LAST-STATUS + MOVE -1 TO WS-RETRY-AFTER-MS + MOVE SPACES TO WS-ERROR-TEXT + MOVE CFG-MAX-ATTEMPTS TO WS-MAX-ATTEMPTS + IF WS-MAX-ATTEMPTS < 1 + MOVE 1 TO WS-MAX-ATTEMPTS + END-IF. + +*> ------------------------------------------------------------------- +*> A trailing slash on CFG-BASE-URL is dropped so it cannot produce the +*> doubled separator that some proxies normalise and others answer 404. +BUILD-URL. + MOVE CFG-BASE-URL TO WS-BASE + COMPUTE WS-BASE-LEN = FUNCTION LENGTH (FUNCTION TRIM (WS-BASE)) + IF WS-BASE = SPACES + MOVE 0 TO WS-BASE-LEN + END-IF + PERFORM UNTIL WS-BASE-LEN <= 0 + OR WS-BASE (WS-BASE-LEN:1) NOT = "/" + SUBTRACT 1 FROM WS-BASE-LEN + END-PERFORM + + MOVE SPACES TO WS-URL-TEXT + IF WS-BASE-LEN > 0 + STRING WS-BASE (1:WS-BASE-LEN) + HTTPQ-PATH (1:HTTPQ-PATH-LEN) + DELIMITED BY SIZE INTO WS-URL-TEXT + END-STRING + ELSE + STRING HTTPQ-PATH (1:HTTPQ-PATH-LEN) + DELIMITED BY SIZE INTO WS-URL-TEXT + END-STRING + END-IF + + MOVE SPACES TO WS-URL + STRING FUNCTION TRIM (WS-URL-TEXT) X"00" + DELIMITED BY SIZE INTO WS-URL + END-STRING + + MOVE SPACES TO WS-USER-AGENT + STRING FUNCTION TRIM (CFG-USER-AGENT) X"00" + DELIMITED BY SIZE INTO WS-USER-AGENT + END-STRING + + MOVE CFG-TIMEOUT-MS TO WS-TIMEOUT-MS + IF WS-TIMEOUT-MS <= 0 + MOVE 10000 TO WS-TIMEOUT-MS + END-IF. + +*> ------------------------------------------------------------------- +ARM-DEADLINE. + MOVE "N" TO WS-HAS-DEADLINE + IF CFG-TOTAL-TIMEOUT-MS > 0 + CALL "cepis_monotonic_ms" USING WS-NOW + COMPUTE WS-DEADLINE = WS-NOW + CFG-TOTAL-TIMEOUT-MS + MOVE "Y" TO WS-HAS-DEADLINE + END-IF. + +*> ------------------------------------------------------------------- +ONE-ATTEMPT. + CALL "cepis_http_get" USING + WS-URL WS-USER-AGENT WS-TIMEOUT-MS + WS-CURL-STATUS + WS-RETRY-AFTER-TEXT WS-RETRY-AFTER-CAP + HTTP-BODY WS-BODY-CAP WS-BODY-LEN + WS-ERROR-TEXT WS-ERROR-CAP + RETURNING WS-RC + END-CALL + + EVALUATE WS-RC + WHEN 0 + MOVE WS-CURL-STATUS TO HTTP-STATUS + MOVE WS-BODY-LEN TO HTTP-BODY-LEN + MOVE HTTP-STATUS TO CEPIS-STATUS-HTTP + PERFORM DECIDE-KEEP + IF WS-KEEP = "Y" + SET CEPIS-OK TO TRUE + MOVE "Y" TO WS-DONE + ELSE + MOVE HTTP-STATUS TO WS-LAST-STATUS + PERFORM PARSE-RETRY-AFTER + END-IF + WHEN 2 + SET CEPIS-INVALID-RESPONSE TO TRUE + MOVE SPACES TO CEPIS-STATUS-MESSAGE + STRING "response from " FUNCTION TRIM (WS-URL-TEXT) + " is larger than the 256 KiB client buffer" + DELIMITED BY SIZE INTO CEPIS-STATUS-MESSAGE + END-STRING + MOVE "Y" TO WS-DONE + WHEN OTHER + MOVE 0 TO WS-LAST-STATUS + MOVE -1 TO WS-RETRY-AFTER-MS + END-EVALUATE. + +*> ------------------------------------------------------------------- +*> Keep this answer, or try again? 200 always; 503 too when the caller +*> asked for it, which is how /health reports itself degraded without +*> being retried three times first. Everything outside the retriable set +*> -- 400 and 404 above all -- is kept for the caller to interpret. +DECIDE-KEEP. + MOVE "Y" TO WS-KEEP + IF HTTP-STATUS = 200 + EXIT PARAGRAPH + END-IF + IF HTTP-STATUS = 503 AND HTTPQ-ALLOW-503 = "Y" + EXIT PARAGRAPH + END-IF + IF HTTP-STATUS = 429 OR HTTP-STATUS = 500 OR HTTP-STATUS = 502 + OR HTTP-STATUS = 503 OR HTTP-STATUS = 504 + MOVE "N" TO WS-KEEP + END-IF. + +*> ------------------------------------------------------------------- +*> Retry-After arrives either as a number of seconds or as an HTTP date. +*> Handling only the first is the common bug, and it makes a client +*> ignore exactly the wait request that matters most. The date form goes +*> to curl_getdate through the shim. +PARSE-RETRY-AFTER. + MOVE -1 TO WS-RETRY-AFTER-MS + IF WS-RETRY-AFTER-TEXT = SPACES + EXIT PARAGRAPH + END-IF + MOVE FUNCTION TRIM (WS-RETRY-AFTER-TEXT) TO WS-RETRY-AFTER-TRIM + COMPUTE WS-TRIM-LEN = + FUNCTION LENGTH (FUNCTION TRIM (WS-RETRY-AFTER-TEXT)) + + MOVE 0 TO WS-DIGITS + MOVE 0 TO WS-JUNK + PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > WS-TRIM-LEN + MOVE WS-RETRY-AFTER-TRIM (WS-I:1) TO WS-C + EVALUATE TRUE + WHEN WS-C >= "0" AND WS-C <= "9" + ADD 1 TO WS-DIGITS + WHEN WS-C = "." + CONTINUE + WHEN OTHER + ADD 1 TO WS-JUNK + END-EVALUATE + END-PERFORM + + IF WS-DIGITS > 0 AND WS-JUNK = 0 + COMPUTE WS-RETRY-AFTER-MS ROUNDED = + FUNCTION NUMVAL (WS-RETRY-AFTER-TRIM) * 1000 + IF WS-RETRY-AFTER-MS < 0 + MOVE 0 TO WS-RETRY-AFTER-MS + END-IF + EXIT PARAGRAPH + END-IF + + MOVE SPACES TO WS-RETRY-AFTER-Z + STRING WS-RETRY-AFTER-TRIM (1:WS-TRIM-LEN) X"00" + DELIMITED BY SIZE INTO WS-RETRY-AFTER-Z + END-STRING + CALL "cepis_http_date_delta" USING WS-RETRY-AFTER-Z WS-SECONDS + RETURNING WS-DATE-RC + END-CALL + IF WS-DATE-RC = 0 + COMPUTE WS-RETRY-AFTER-MS = WS-SECONDS * 1000 + END-IF. + +*> ------------------------------------------------------------------- +COMPUTE-WAIT. + IF WS-RETRY-AFTER-MS >= 0 + MOVE WS-RETRY-AFTER-MS TO WS-WAIT-MS + IF WS-WAIT-MS > CFG-BACKOFF-MAX-MS + MOVE CFG-BACKOFF-MAX-MS TO WS-WAIT-MS + END-IF + EXIT PARAGRAPH + END-IF + + MOVE WS-ATTEMPT TO WS-EXPONENT + IF WS-EXPONENT > 20 + MOVE 20 TO WS-EXPONENT + END-IF + COMPUTE WS-CEILING = + CFG-BACKOFF-BASE-MS * (2 ** (WS-EXPONENT - 1)) + IF WS-CEILING > CFG-BACKOFF-MAX-MS + MOVE CFG-BACKOFF-MAX-MS TO WS-CEILING + END-IF + COMPUTE WS-RANDOM = FUNCTION RANDOM + COMPUTE WS-WAIT-MS ROUNDED = WS-RANDOM * WS-CEILING. + +*> ------------------------------------------------------------------- +CLASSIFY-FAILURE. + MOVE SPACES TO CEPIS-STATUS-MESSAGE + EVALUATE TRUE + WHEN WS-LAST-STATUS = 429 + SET CEPIS-RATE-LIMITED TO TRUE + MOVE WS-LAST-STATUS TO CEPIS-STATUS-HTTP + IF WS-RETRY-AFTER-MS > 999999999 + MOVE 999999999 TO CEPIS-STATUS-RETRY-AFTER-MS + ELSE + MOVE WS-RETRY-AFTER-MS TO CEPIS-STATUS-RETRY-AFTER-MS + END-IF + STRING "rate limit reached at " + FUNCTION TRIM (WS-URL-TEXT) + DELIMITED BY SIZE INTO CEPIS-STATUS-MESSAGE + END-STRING + WHEN WS-LAST-STATUS > 0 + SET CEPIS-SERVER-ERROR TO TRUE + MOVE WS-LAST-STATUS TO CEPIS-STATUS-HTTP + STRING "the API answered " WS-LAST-STATUS " at " + FUNCTION TRIM (WS-URL-TEXT) + DELIMITED BY SIZE INTO CEPIS-STATUS-MESSAGE + END-STRING + WHEN OTHER + SET CEPIS-NETWORK-ERROR TO TRUE + STRING "could not reach " FUNCTION TRIM (WS-URL-TEXT) + ": " FUNCTION TRIM (WS-ERROR-TEXT) + DELIMITED BY SIZE INTO CEPIS-STATUS-MESSAGE + END-STRING + END-EVALUATE. + +END PROGRAM CEPIS-REQUEST. |