Ejemplos

Ejecute código COBOL fuera del mainframe!

Aquí podrá encontrar un conjunto de programas de ejemplo que le permitirán validar técnicamente las posibilidades de migración de su código mainframe a un arquitectura open.

El código se ha simplificado con el objetivo de hacerlo entendible por cualquier persona con unos conocimientos mínimos de programación.

Puede descargarse el código accediendo al siguiente proyecto en GitHub

1 - Hello World

Convierta un programa COBOL en una API REST.

Cómo darle una nueva vida a su código COBOL, aprenda a construir APIs REST usando Go cgo.

package main /* #cgo CFLAGS: -I${SRCDIR}/include #cgo LDFLAGS: ${SRCDIR}/libs/hello.o -L/opt/homebrew/Cellar/gnucobol/3.2/lib -lcob #include <stdlib.h> #include <stdio.h> #include <string.h> #include "hello.h" extern void cob_init(int argc,char** argv); */ import "C" import ( "net/http" "unsafe" "github.com/gin-gonic/gin" ) func callhello(d string) string { inputName := C.CString(d) defer C.free(unsafe.Pointer(inputName)) outputParm := C.CString("") defer C.free(unsafe.Pointer(outputParm)) returnCode := C.hello(inputName, outputParm) if returnCode == 0 || returnCode == 2 { return C.GoString(outputParm) } else { return "ERROR FROM COBOL" } } func main() { C.cob_init(C.int(0), nil) router := gin.Default() router.GET("/hello", getName) router.GET("/hello/:name", getName) router.Run("localhost:8080") } func getName(c *gin.Context) { d := c.Param("name") o := callhello(d) c.IndentedJSON(http.StatusOK, gin.H{"output-parm": o}) }

Para más información consulte Comience a usar driver8.

2 - COBOL gRPC server

Construya un gRPC server a partir de la COPYBOOK.

Transforme una COPYBOOK en un mensaje proto. Sustituya el CICS IMS por un moderno y eficiente mecanismo basado en RPC (HTTP/2, compresión, cifrado, etc.).

En este ejemplo vamos a implementar nuestro programa COBOL “Hello, World” como un servidor gRPC.

IDENTIFICATION DIVISION. PROGRAM-ID. hello. ENVIRONMENT DIVISION. CONFIGURATION SECTION. DATA DIVISION. WORKING-STORAGE SECTION. * Declare program variables LINKAGE SECTION. * Data to share with COBOL subroutines 01 RECORD-TYPE. 05 INPUT-NAME PIC X(10). 05 OUTPUT-PARM. 10 PARM1 PIC X(07). 10 PARM2 PIC X(10). PROCEDURE DIVISION USING RECORD-TYPE. MOVE "Hello," TO PARM1. IF INPUT-NAME IS EQUAL TO (SPACES OR LOW-VALUES) MOVE "World" TO PARM2 MOVE 2 TO RETURN-CODE ELSE MOVE INPUT-NAME TO PARM2 MOVE 0 TO RETURN-CODE END-IF. GOBACK.

Cree la siguiente estructura de directorios:

├── d8grpc
│   └── hello_client
│   └── hello_server
│   └── hello
│   go.mod
│   go.sum

A continuación crearemos la definición del mensaje proto que nos servirá para exponer la COPYBOOK del programa COBOL. Para ello cree un fichero con el nombre hello.proto en el directorio d8grpc/hello y copie el siguiente fichero.

syntax = "proto3"; option go_package = "github.com/driver8soft/examples/d8grpc/hello"; package hello; // d8grpc hello service definition. service D8grpc { // Sends a greeting rpc Hello (MsgReq) returns (MsgRes) {} } // The request message containing the user's name. message MsgReq { string hello_name = 1; } // The response message containing the greetings message MsgRes { string response = 1; }

Los campos de la COPYBOOK COBOL:

  • INPUT-NAME
  • OUTPUT-PARM

Están definidos como tipo CHAR (con longitudes 10 y 17) y se convierten a string.

Para compilar el mensaje proto ejecute el siguiente comando

protoc --go_out=. --go_opt=paths=source_relative \ --go-grpc_out=. --go-grpc_opt=paths=source_relative \ hello/hello.proto

Instale antes la utilidad de compilación de mensajes proto para el lenguaje Go

Para ello siga las siguientes instrucciones

Vamos a crear el servidor gRPC que realizará la llamada a la subrutina COBOL, en este caso la llamada se realizará de manera dinámica. Cree el fichero main.go en el directorio d8grpc/hello_server y copie el siguiente fichero.

package main /* #include <stdlib.h> #include <stdio.h> #include <string.h> #include <libcob.h> #cgo CFLAGS: -I/opt/homebrew/Cellar/gnucobol/3.2/include #cgo LDFLAGS: -L/opt/homebrew/Cellar/gnucobol/3.2/lib -lcob static void* allocArgv(int argc) { return malloc(sizeof(char *) * argc); } */ import "C" import ( "context" "errors" "flag" "fmt" "log" "net" "time" "unsafe" pb "github.com/driver8soft/examples/d8grpc/hello" "google.golang.org/grpc" ) var ( port = flag.Int("port", 50051, "The server port") ) type server struct { pb.UnimplementedD8GrpcServer } func (s *server) Hello(ctx context.Context, in *pb.MsgReq) (out *pb.MsgRes, err error) { start := time.Now() // define argc, argv c_argc := C.int(1) c_argv := (*[0xfff]*C.char)(C.allocArgv(c_argc)) defer C.free(unsafe.Pointer(c_argv)) c_argv[0] = C.CString(in.GetHelloName()) // check COBOL program n := C.cob_resolve(C.CString("hello")) if n == nil { err := errors.New("COBOL: program not found") log.Println(err) return &pb.MsgRes{}, err } //Call COBOL program log.Println("INFO: program hello started") ret := C.cob_call(C.CString("hello"), c_argc, (*unsafe.Pointer)(unsafe.Pointer(c_argv))) log.Printf("INFO: program hello return-code %v", ret) //COBOL COPYBOOK is converted to Go String using COPYBOOK length output := C.GoStringN(c_argv[0], 27) elapsed := time.Since(start) log.Printf("INFO: Hello elapsed time %s", elapsed) return &pb.MsgRes{Response: output[9:]}, nil } func main() { flag.Parse() // d8 Initialize gnucobol C.cob_init(C.int(0), nil) lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) if err != nil { log.Fatalf("ERROR: failed to listen: %v", err) } var opts []grpc.ServerOption grpcServer := grpc.NewServer(opts...) pb.RegisterD8GrpcServer(grpcServer, &server{}) log.Printf("INFO: server listening at %v", lis.Addr()) if err := grpcServer.Serve(lis); err != nil { log.Fatalf("ERROR: failed to serve: %v", err) } }

Compile la subrutina COBOL mediante el siguiente comando. El resultado será un módulo (shared library) que podremos llamar de manera dinámica desde el servidor Go gRPC mediante cgo.

cobc -m hello.cbl

El fichero resultante (*.so, *.dylib) puede dejarse en el directorio d8grpc/hello_server

Si decide dejar el módulo COBOL en otro directorio recuerde definirlo (export COB_LIBRARY_PATH=/…my_library…/)

Abra un terminal y ejecute el servidor gRPC mediante el siguiente comando

go run .

Por último, crearemos un cliente Go para realizar la llamada a nuestro servicio gRPC COBOL. Cree el fichero main.go en el directorio d8grpc/hello_client y copie el siguiente fichero.

package main import ( "context" "flag" "log" pb "github.com/driver8soft/examples/d8grpc/hello" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) var ( addr = flag.String("addr", "localhost:50051", "the address to connect to") ) var ( name = flag.String("name", "", "name") ) func main() { flag.Parse() // Set up a connection to the server. conn, err := grpc.NewClient(*addr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() client := pb.NewD8GrpcClient(conn) // Contact the server and print out its response. r, err := client.Hello(context.Background(), &pb.MsgReq{HelloName: *name}) if err == nil { log.Printf("Output: %s", r.GetResponse()) } else { log.Printf("ERROR: %v", err) } }

Para probar nuestro servicio COBOL gRPC abra un nuevo terminal y ejecute el comando.

go run main.go -name=Hooper

3 - Playing with PostgreSQL

Un ejemplo COBOL PostgreSQL.

¿El lenguaje COBOL sólo puede acceder a DB2?

En este sencillo ejemplo accederemos a una base de datos PostgreSQL desde un programa COBOL.

Sus programas pueden ser pre-compilados (EXEC SQL) para acceder a distintas bases de datos SQL

  • Oracle Pro*Cobol
  • IBM DB2 Cobol precompiler
  • OpenESQL para PostgreSQL

Para poder ejecutar este programa es necesario instalar PostgreSQL y crear la base de datos de ejemplo (dvdrental). Puede encontrar las instrucciones de como hacerlo aquí.

***************************************************************** * Connect and get data from PostgreSQL * Sample DB "dvdrental" table "actor" ***************************************************************** IDENTIFICATION DIVISION. PROGRAM-ID. pgcobol. AUTHOR. DATA DIVISION. WORKING-STORAGE SECTION. * CONNECT TO POSGRESQL 01 CONN-STR. 05 FILLER PIC X(20) VALUE "dbname=dvdrental ". 05 FILLER PIC X(20) VALUE "user=XXXXXXXX ". 05 FILLER PIC X(20) VALUE "password=XXXXXXX ". 05 FILLER PIC X(20) VALUE "host=localhost ". 05 FILLER PIC X(20) VALUE "port=5432 ". 05 FILLER PIC X(20) VALUE "sslmode=disable ". 05 FILLER PIC X(01) VALUE LOW-VALUES. 01 CONNECTION USAGE POINTER. 01 CONN-STATUS USAGE BINARY-LONG. * DECLARE CURSOR 01 SQL-QUERY. 05 SQL-QUERY-DATA PIC X(4096) VALUE SPACES. 05 FILLER PIC X(01) VALUE LOW-VALUES. 01 DB-CURSOR USAGE POINTER. * SQL ERROR 01 SQL-STATUS USAGE BINARY-LONG. 01 SQL-ERROR-PTR USAGE POINTER. 01 SQL-ERROR-STR PIC X(4096) BASED. 01 SQL-ERROR-MSG PIC X(100) VALUE SPACES. * COUNTER 01 ROW-COUNTER USAGE BINARY-LONG. 01 COLUMN-COUNTER USAGE BINARY-LONG. * FETCH 01 RESULT-PTR USAGE POINTER. 01 RESULT-STR PIC X(4096) BASED. 01 RESULT-DATA PIC X(4096) VALUE SPACES. 01 TABLE-ROW. 02 actor_id PIC 9(4) VALUE ZEROS. 02 first_name PIC X(45) VALUE SPACES. 02 last_name PIC X(45) VALUE SPACES. 02 last_update PIC X(22) VALUE SPACES. * AUX VARIABLES 01 DB-ROW PIC 9(7) VALUE ZEROS. 01 DB-COLUMN PIC 9(3) VALUE ZEROS. *> ********************************************************************* PROCEDURE DIVISION. PERFORM CONNECT-DB. MOVE "SELECT actor_id, first_name, " & "last_name, last_update " & "FROM actor;" TO SQL-QUERY-DATA. PERFORM DECLARE-CURSOR. PERFORM ROW-COUNT. PERFORM COLUMN-COUNT. * ITERATE OVER ROWS PERFORM VARYING DB-ROW FROM 0 BY 1 UNTIL DB-ROW >= ROW-COUNTER PERFORM VARYING DB-COLUMN FROM 0 BY 1 UNTIL DB-COLUMN >= COLUMN-COUNTER PERFORM ROW-FETCH END-PERFORM DISPLAY actor_id " - " first_name " - " last_name " - " last_update END-PERFORM. PERFORM DISCONNECT. GOBACK. * CONNECT-DB. * CONNECT AND CHECK DB STATUS CALL "PQconnectdb" USING CONN-STR RETURNING CONNECTION. CALL "PQstatus" USING BY VALUE CONNECTION RETURNING CONN-STATUS. IF CONN-STATUS NOT EQUAL 0 THEN DISPLAY "Connection error! " CONN-STATUS STOP RUN END-IF. DISCONNECT. * CLOSE CONNECTION DB CALL "PQfinish" USING BY VALUE CONNECTION RETURNING OMITTED. DECLARE-CURSOR. * OPEN CURSOR CALL "PQexec" USING BY VALUE CONNECTION BY REFERENCE SQL-QUERY RETURNING DB-CURSOR END-CALL. CALL "PQresultStatus" USING BY VALUE DB-CURSOR RETURNING SQL-STATUS. CALL "PQresStatus" USING BY VALUE SQL-STATUS RETURNING SQL-ERROR-PTR. SET ADDRESS OF SQL-ERROR-STR TO SQL-ERROR-PTR. STRING SQL-ERROR-STR DELIMITED BY x"00" INTO SQL-ERROR-MSG END-STRING. IF SQL-STATUS NOT EQUAL 2 THEN DISPLAY "Open Cursor error! " SQL-STATUS SQL-ERROR-MSG STOP RUN END-IF. DISPLAY "sql_status: " SQL-STATUS " sql_error: " SQL-ERROR-MSG. ROW-COUNT. * GET NUMBER OF ROWS CALL "PQntuples" USING BY VALUE DB-CURSOR RETURNING ROW-COUNTER. DISPLAY "number of rows: " ROW-COUNTER. COLUMN-COUNT. * GET NUMBER OF COLUMNS CALL "PQnfields" USING BY VALUE DB-CURSOR RETURNING COLUMN-COUNTER. DISPLAY "number of fields: " COLUMN-COUNTER. ROW-FETCH. *> FETCH CALL "PQgetvalue" USING BY VALUE DB-CURSOR BY VALUE DB-ROW BY VALUE DB-COLUMN RETURNING RESULT-PTR END-CALL SET ADDRESS OF RESULT-STR TO RESULT-PTR INITIALIZE RESULT-DATA. STRING RESULT-STR DELIMITED BY x"00" INTO RESULT-DATA END-STRING. EVALUATE DB-COLUMN WHEN 0 MOVE RESULT-DATA TO actor_id WHEN 1 MOVE RESULT-DATA TO first_name WHEN 2 MOVE RESULT-DATA TO last_name WHEN 3 MOVE RESULT-DATA TO last_update END-EVALUATE.

Recuerde modificar los campos de WORKING CONN-STR con un usuario y password válidos para la conexión a la base de datos

Las funciones utilizadas por el programa COBOL necesitan la librería de postgreSQL “libpq”, localice donde está instalada dicha librería y añadala en el momento de compilar el programa, por ejemplo:

cobc -x pgcobol.cbl -L/Library/postgreSQL/16/lib -lpq

4 - Calling COBOL containers

Llame a programas COBOL remotos.

De manera equivalente al mecanismo del CICS para llamar a programas remotos (EXEC CICS LINK), realice llamadas entre programas COBOL desplegados en distintos contenedores.

A continuación se describe gráficamente el flujo de ejecución

loanmain.cbl <–> d8link.go <———————–> main.go <–> loancalc.cbl

  1. El programa COBOL loanmain.cbl realiza una llamada (CALL) al conector gRPC d8link, esta simula una sentencia EXEC CICS LINK:
  • Se define el programa al que se quiere llamar
  • El área de intercambio de datos (COMMAREA)
  • Y la longitud de la misma
  1. El conector gRPC d8link recibe los datos (COMMAREA) y llama al microservicio COBOL correspondiente
  2. El controller gPRC (main.go) gestiona el mensaje proto, lo convierte a una estructura compatible y llama al programa COBOL loancalc.cbl
  3. El programa COBOL actualiza el área de datos y devuelve el control al controlador gRPC
  4. Los datos son envíados de vuelta al conector d8link que los copia sobre el área de memoria definida por el programa COBOL

Cree una estructura de directorios como la siguiente:

├── d8link
│   └── link_client
│   └── link_server
│   └── link
│   go.mod
│   go.sum

En el directorio link definiremos nuestro mensaje proto (link.proto)

syntax = "proto3"; option go_package = "github.com/driver8soft/examples/d8link/link"; package link; // The Link service definition. service LinkService { rpc CommArea (CommReq) returns (CommResp) {} } // The request message containing program to link, commarea and commarea length. message CommReq { string link_prog = 1; int32 comm_len = 2; bytes input_msg = 3; } // The response message containing commarea message CommResp { bytes output_msg = 1; }

A continuación crearemos el programa d8link.go sobre el directorio link_client

package main /* #include <string.h> #include <stdlib.h> */ import "C" import ( "context" "flag" "log" "unsafe" pb "github.com/driver8soft/examples/d8link/link" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) var ( addr = flag.String("addr", "localhost:50051", "the address to connect to") ) //export D8link func D8link(c_program *C.char, c_commarea *C.char, c_commlen *C.int) C.int { flag.Parse() // C variables to Go variables program := C.GoStringN(c_program, 8) // max length of COBOL mainframe program = 8 commarea := C.GoBytes(unsafe.Pointer(c_commarea), *c_commlen) commlen := int32(*c_commlen) log.Println("INFO: Call program -", program) // Set up a connection to the server. conn, err := grpc.NewClient(*addr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() client := pb.NewLinkServiceClient(conn) // Contact the server r, err := client.CommArea(context.Background(), &pb.CommReq{LinkProg: program, CommLen: commlen, InputMsg: commarea}) if err != nil { log.Fatalf("ERROR: calling program - %s - %v", program, err) } outMsg := r.GetOutputMsg() C.memcpy(unsafe.Pointer(c_commarea), unsafe.Pointer(&outMsg[0]), C.size_t(commlen)) return 0 } func main() { }

Vamos a exportar la función D8link para que pueda ser llamada desde un programa COBOL, para ello es necesario compilarla utilizando la opción c-shared de Go

El compilador de Go generará un objeto (D8link.dylib D8link.so) y un fichero (D8link.h) que serán llamados dinámicamente desde el código COBOL

Y para finalizar crearemos el servidor gRPC (main.go) en el directorio link_server que será el encargado de recibir el mensaje proto y llamar al programa COBOL destino.

package main /* #include <stdlib.h> #include <stdio.h> #include <string.h> #include <libcob.h> #cgo CFLAGS: -I/opt/homebrew/Cellar/gnucobol/3.2/include #cgo LDFLAGS: -L/opt/homebrew/Cellar/gnucobol/3.2/lib -lcob static void* allocArgv(int argc) { return malloc(sizeof(char *) * argc); } */ import "C" import ( "context" "flag" "fmt" "log" "net" "strings" "time" "unsafe" pb "github.com/driver8soft/examples/d8link/link" "google.golang.org/grpc" ) var ( port = flag.Int("port", 50051, "The server port") ) type server struct { pb.UnimplementedLinkServiceServer } func (s *server) CommArea(ctx context.Context, in *pb.CommReq) (out *pb.CommResp, err error) { start := time.Now() // remove trailing spaces from program name program := strings.TrimSpace(in.GetLinkProg()) c_program := C.CString(program) defer C.free(unsafe.Pointer(c_program)) c_commlen := C.int(in.GetCommLen()) // allocate argc & argv variables c_argc := C.int(1) c_argv := (*[0xfff]*C.char)(C.allocArgv(c_argc)) defer C.free(unsafe.Pointer(c_argv)) c_argv[0] = C.CString(string(in.GetInputMsg())) defer C.free(unsafe.Pointer(c_argv[0])) // check COBOL program n := C.cob_resolve(c_program) if n == nil { log.Println("ERROR: Module not found. Program name =", program) } else { log.Printf("INFO: %s started", program) ret := C.cob_call(c_program, c_argc, (*unsafe.Pointer)(unsafe.Pointer(c_argv))) log.Printf("INFO: %s return-code %v", program, ret) } c_msg_output := C.GoStringN(c_argv[0], c_commlen) elapsed := time.Since(start) log.Printf("INFO: %s elapsed time %s", program, elapsed) return &pb.CommResp{OutputMsg: []byte(c_msg_output)}, nil } func main() { flag.Parse() // d8 Initialize gnucobol C.cob_init(C.int(0), nil) lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) if err != nil { log.Fatalf("ERROR: failed to listen: %v", err) } grpcServer := grpc.NewServer() pb.RegisterLinkServiceServer(grpcServer, &server{}) log.Printf("INFO: server listening at %v", lis.Addr()) if err := grpcServer.Serve(lis); err != nil { log.Fatalf("ERROR: failed to serve: %v", err) } }

Pruebe a realizar llamadas remotas entre programas COBOL intercambiando un área de datos (COPYBOOK). Para ello recuerde que:

  • El programa llamador debe compilarse para generar un ejecutable (opción -x GNUCobol)
  • El programa llamado debe compilarse para generar un objeto (opción -m GNUCobol)
  • Ambos programas deben compilarse utilizando el mismo “byteorder” para compartir datos binarios
  • Para simplificar la prueba los programas COBOL puede residir en los directorios definidos anteriormente (link_client link_server)

Puede utilizar los programas COBOL de ejemplo loanmain.cbl y loancalc.cbl.

****************************************************************** * * Loan Calculator Main Program * ========================== * * A sample program to demonstrate how to create a gRPC COBOL * microservice. * ****************************************************************** IDENTIFICATION DIVISION. PROGRAM-ID. loanmain. ENVIRONMENT DIVISION. CONFIGURATION SECTION. DATA DIVISION. FILE SECTION. WORKING-STORAGE SECTION. * Declare program variables 01 PROG-NAME PIC X(8) VALUE "loancalc". 01 COMMLEN PIC 9(9) COMP. 01 COMMAREA. 05 INPUT-MSG. 10 PRIN-AMT PIC S9(7) USAGE IS DISPLAY. 10 INT-RATE PIC S9(2)V9(2) USAGE IS DISPLAY. 10 TIMEYR PIC S9(2) USAGE IS DISPLAY. 05 OUTPUT-MSG. 10 PAYMENT PIC S9(7)V9(2) USAGE IS DISPLAY. 10 ERROR-MSG PIC X(20). PROCEDURE DIVISION. * code goes here! INITIALIZE COMMAREA. DISPLAY "Compound Interest Calculator" DISPLAY "Principal amount: " WITH NO ADVANCING. ACCEPT PRIN-AMT. DISPLAY "Interest rate: " WITH NO ADVANCING. ACCEPT INT-RATE. DISPLAY "Number of years: " WITH NO ADVANCING. ACCEPT TIMEYR. COMPUTE COMMLEN = LENGTH OF COMMAREA. CALL "D8link" USING PROG-NAME COMMAREA COMMLEN. DISPLAY "Error Msg: " ERROR-MSG. DISPLAY "Couta: " PAYMENT. GOBACK.
****************************************************************** * * Loan Calculator Subroutine * ========================== * * A sample program to demonstrate how to create a gRPC COBOL * microservice. * ****************************************************************** IDENTIFICATION DIVISION. PROGRAM-ID. loancalc. ENVIRONMENT DIVISION. CONFIGURATION SECTION. DATA DIVISION. FILE SECTION. WORKING-STORAGE SECTION. * Declare program variables 01 WS-MSG. 05 WS-ERROR PIC X(01). 05 WS-MSG00 PIC X(20) VALUE 'OK'. 05 WS-MSG10 PIC X(20) VALUE 'INVALID INT. RATE'. 05 WS-MSG12 PIC X(20) VALUE 'INVALID NUMBER YEARS'. 01 AUX-VARS. 05 MONTHLY-RATE USAGE IS COMP-2. 05 AUX-X USAGE IS COMP-2. 05 AUX-Y USAGE IS COMP-2. 05 AUX-Z USAGE IS COMP-2. LINKAGE SECTION. * Data to share with COBOL subroutines 01 LOAN-PARAMS. 05 INPUT-MSG. 10 PRIN-AMT PIC S9(7) USAGE IS DISPLAY. 10 INT-RATE PIC S9(2)V9(2) USAGE IS DISPLAY. 10 TIMEYR PIC S9(2) USAGE IS DISPLAY. 05 OUTPUT-MSG. 10 PAYMENT PIC S9(7)V9(2) USAGE IS DISPLAY. 10 ERROR-MSG PIC X(20). PROCEDURE DIVISION USING BY REFERENCE LOAN-PARAMS. * code goes here! 000-MAIN. MOVE "N" TO WS-ERROR. DISPLAY "PRIN-AMT: " PRIN-AMT. DISPLAY "INT-RATE: " INT-RATE. DISPLAY "TIMEYR: " TIMEYR. PERFORM 100-INIT. IF WS-ERROR = 'N' PERFORM 200-PROCESS END-IF. PERFORM 300-WRAPUP. 100-INIT. IF INT-RATE <= 0 MOVE WS-MSG10 TO ERROR-MSG MOVE 10 TO RETURN-CODE MOVE 'Y' TO WS-ERROR ELSE IF TIMEYR <= 0 MOVE WS-MSG12 TO ERROR-MSG MOVE 12 TO RETURN-CODE MOVE 'Y' TO WS-ERROR END-IF END-IF. 200-PROCESS. INITIALIZE AUX-VARS. COMPUTE MONTHLY-RATE = (INT-RATE / 12 / 100). COMPUTE AUX-X = ((1 + MONTHLY-RATE) ** (TIMEYR*12)). COMPUTE AUX-Y = AUX-X * MONTHLY-RATE. COMPUTE AUX-Z = (AUX-X - 1) / AUX-Y. COMPUTE PAYMENT = PRIN-AMT / AUX-Z. MOVE WS-MSG00 TO ERROR-MSG. MOVE 0 TO RETURN-CODE. DISPLAY "PAYMENT: " PAYMENT. DISPLAY "ERROR-MSG: " ERROR-MSG. 300-WRAPUP. GOBACK.

5 - COBOL & Kafka

Convierta su programa COBOL en un Kafka consumer/producer.

Quiere integrar sus programas COBOL en un modelo de proceso basado en eventos.

Aprenda cómo convertir un programa COBOL en un Kafka consumer / producer.

Desde el programa COBOL realizaremos una llamada al módulo D8kafka pasándole:

  • El topic Kafka
  • Una lista de valores (key : value) separados por comas
****************************************************************** * * Loan kafka producer * ========================== * * ****************************************************************** IDENTIFICATION DIVISION. PROGRAM-ID. cuotak. ENVIRONMENT DIVISION. DATA DIVISION. FILE SECTION. WORKING-STORAGE SECTION. 01 WS-LOAN. 05 WS-AMT PIC 9(7)V9(2). 05 WS-INT PIC 9(2)V9(2). 05 WS-YEAR PIC 9(2). ****************************************************************** 01 KAFKA. 05 KAFKA-TOPIC PIC X(05) VALUE "loans". 05 FILLER PIC X(1) VALUE LOW-VALUES. 05 KAFKA-KEY. 10 KAFKA-KEY1 PIC X(15) VALUE "PrincipalAmount". 10 FILLER PIC X(1) VALUE ",". 10 KAFKA-KEY2 PIC X(12) VALUE "InterestRate". 10 FILLER PIC X(1) VALUE ",". 10 KAFKA-KEY1 PIC X(09) VALUE "TimeYears". 10 FILLER PIC X(1) VALUE LOW-VALUES. 05 KAFKA-VALUE. 10 KAFKA-AMT-VALUE PIC zzzzzz9.99. 10 FILLER PIC X(1) VALUE ",". 10 KAFKA-INT-VALUE PIC z9.99. 10 FILLER PIC X(1) VALUE ",". 10 KAFKA-YEAR-VALUE PIC zz. 10 FILLER PIC X(1) VALUE LOW-VALUES. PROCEDURE DIVISION. INITIALIZE WS-LOAN. DISPLAY "Amount: " WITH NO ADVANCING. ACCEPT WS-AMT. DISPLAY "Interest: " WITH NO ADVANCING. ACCEPT WS-INT. DISPLAY "Number of Years: " WITH NO ADVANCING. ACCEPT WS-YEAR. MOVE WS-AMT TO KAFKA-AMT-VALUE. MOVE WS-INT TO KAFKA-INT-VALUE. MOVE WS-YEAR TO KAFKA-YEAR-VALUE. CALL "D8kafka" USING KAFKA-TOPIC KAFKA-KEY KAFKA-VALUE. DISPLAY "Return-code: " RETURN-CODE. GOBACK.

A continuación se muestra una versión simplificada de ejemplo del módulo d8kafka

package main /* #include <string.h> #include <stdlib.h> */ import "C" import ( "encoding/json" "fmt" "os" "strings" "github.com/confluentinc/confluent-kafka-go/kafka" ) type Kdata struct { Key string `json:"key"` Value string `json:"value"` } //export D8kafka func D8kafka(c_topic *C.char, c_key *C.char, c_value *C.char) C.int { keys := strings.Split(C.GoString(c_key), ",") values := strings.Split(C.GoString(c_value), ",") data := make([]Kdata, len(keys)) for i := 0; i < len(keys); i++ { data[i] = Kdata{Key: keys[i], Value: values[i]} } KafkaMsg, _ := json.Marshal(data) topic := C.GoString(c_topic) p, err := kafka.NewProducer(&kafka.ConfigMap{ "bootstrap.servers": "localhost:29092", "client.id": "client", "acks": "all"}, ) if err != nil { fmt.Printf("ERROR: Failed to create producer: %s\n", err) os.Exit(1) } delivery_chan := make(chan kafka.Event, 1000) err = p.Produce( &kafka.Message{ TopicPartition: kafka.TopicPartition{Topic: &topic, Partition: kafka.PartitionAny}, Value: []byte(KafkaMsg), }, delivery_chan, ) if err != nil { fmt.Printf("ERROR: Failed to produce message: %s\n", err) os.Exit(1) } e := <-delivery_chan m := e.(*kafka.Message) if m.TopicPartition.Error != nil { fmt.Printf("ERROR: Delivery failed: %v\n", m.TopicPartition.Error) } else { fmt.Printf("INFO: Delivered message to topic %s [%d] at offset %v\n", *m.TopicPartition.Topic, m.TopicPartition.Partition, m.TopicPartition.Offset) } close(delivery_chan) return 0 } func main() { }

Para consumir el topic kafka desde un programa Go puede utilizar el siguiente ejemplo:

package main import ( "fmt" "os" "github.com/confluentinc/confluent-kafka-go/kafka" ) var topic string = "loans" var run bool = true func main() { consumer, err := kafka.NewConsumer(&kafka.ConfigMap{ "bootstrap.servers": "localhost:29092", "group.id": "sample", "auto.offset.reset": "smallest"}, ) if err != nil { fmt.Printf("ERROR: Failed to create consumer: %s\n", err) os.Exit(1) } err = consumer.Subscribe(topic, nil) if err != nil { fmt.Printf("ERROR: Failed to subscribe: %s\n", err) os.Exit(1) } for run { ev := consumer.Poll(100) switch e := ev.(type) { case *kafka.Message: fmt.Printf("INFO: %s", e.Value) case kafka.Error: fmt.Printf("%% ERROR: %v\n", e) run = false } } consumer.Close() }

Para ejecutar una prueba es necesario tener instalado Kafka

Un manera sencilla de hacerlo es utilizar Docker (docker-compose.yml) para configurar un entorno mínimo de pruebas con zookeeper y kafka

6 - JCL to DAG

Transforme un JCL en un fichero de configuración para ejecutar un programa batch.

Vamos a convertir un paso de un JCL en un fichero de configuración (yaml).

//JOB1    JOB (123),CLASS=C,MSGCLASS=S,MSGLEVEL=(1,1),NOTIFY=&SYSUID
//*
//STEP01   EXEC PGM=BCUOTA
//INFILE   DD   DSN=DEV.APPL1.TEST,DISP=SHR
//OUTFILE  DD   DSN=DEV.APPL1.CUOTA,
//              DISP=(NEW,CATLG,DELETE),VOLUME=SER=SHARED,
//              SPACE=(CYL,(1,1),RLSE),UNIT=SYSDA,
//              DCB=(RECFM=FB,LRECL=80,BLKSIZE=800)
//*

Cree un fichero step.yaml y copie el siguiente código.

--- stepname: "step1" exec: pgm: "bcuota" dd: - name: "infile" dsn: "test.txt" disp: "shr" normaldisp: "catlg" abnormaldisp: "catlg" - name: "outfile" dsn: "cuota.txt" disp: "new" normaldisp: "catlg" abnormaldisp: "delete"

A continuación ejecutaremos un programa batch de lectura/escritura de ficheros usando esta configuración. El programa principal bcuota.cbl lee un fichero de entrada, llama a la rutina COBOL loancalc.cbl para calcular la cuota a pagar de un préstamo y escribe el resultado en el fichero de salida.

****************************************************************** * * Loan Calculator Batch * ========================== * * ****************************************************************** IDENTIFICATION DIVISION. PROGRAM-ID. bcuota. ENVIRONMENT DIVISION. INPUT-OUTPUT SECTION. FILE-CONTROL. SELECT LOAN ASSIGN TO "infile" ORGANIZATION IS LINE SEQUENTIAL ACCESS IS SEQUENTIAL. SELECT CUOTA ASSIGN TO "outfile" ORGANIZATION IS LINE SEQUENTIAL ACCESS IS SEQUENTIAL. DATA DIVISION. FILE SECTION. FD LOAN. 01 LOAN-FILE PIC X(26). FD CUOTA. 01 CUOTA-FILE. 05 CUOTA-ACC PIC X(10). 05 CUOTA-PAY PIC 9(7)V9(2). WORKING-STORAGE SECTION. 01 WS-LOAN. 05 WS-ACC PIC X(10). 05 FILLER PIC X(1). 05 WS-AMT PIC 9(7). 05 FILLER PIC X(1). 05 WS-INT PIC 9(2)V9(2). 05 FILLER PIC X(1). 05 WS-YEAR PIC 9(2). 01 WS-EOF PIC X(1) VALUE "N". 01 WS-COUNTER PIC 9(9) VALUE ZEROES. **************************************************************** 01 LOAN-PARAMS. 05 INPUT-MSG. 10 PRIN-AMT PIC S9(7) USAGE IS DISPLAY. 10 INT-RATE PIC S9(2)V9(2) USAGE IS DISPLAY. 10 TIMEYR PIC S9(2) USAGE IS DISPLAY. 05 OUTPUT-MSG. 10 PAYMENT PIC S9(7)V9(2) USAGE IS DISPLAY. 10 ERROR-MSG PIC X(20). PROCEDURE DIVISION. OPEN INPUT LOAN. OPEN OUTPUT CUOTA. PERFORM UNTIL WS-EOF='Y' READ LOAN INTO WS-LOAN AT END MOVE 'Y' TO WS-EOF NOT AT END MOVE WS-AMT TO PRIN-AMT MOVE WS-INT TO INT-RATE MOVE WS-YEAR TO TIMEYR CALL "loancalc" USING LOAN-PARAMS ADD 1 TO WS-COUNTER MOVE WS-ACC TO CUOTA-ACC MOVE PAYMENT TO CUOTA-PAY WRITE CUOTA-FILE END-WRITE END-READ END-PERFORM. CLOSE LOAN. CLOSE CUOTA. DISPLAY "TOTAL RECORDS PROCESSED: " WS-COUNTER. GOBACK.

La rutina loancalc.cbl se ha modificado para eliminar la escritura en el log del sistema

****************************************************************** * * Loan Calculator Subroutine * ========================== * * A sample program to demonstrate how to create a gRPC COBOL * microservice. * ****************************************************************** IDENTIFICATION DIVISION. PROGRAM-ID. loancalc. ENVIRONMENT DIVISION. CONFIGURATION SECTION. DATA DIVISION. FILE SECTION. WORKING-STORAGE SECTION. * Declare program variables 01 WS-MSG. 05 WS-ERROR PIC X(01). 05 WS-MSG00 PIC X(20) VALUE 'OK'. 05 WS-MSG10 PIC X(20) VALUE 'INVALID INT. RATE'. 05 WS-MSG12 PIC X(20) VALUE 'INVALID NUMBER YEARS'. 01 AUX-VARS. 05 MONTHLY-RATE USAGE IS COMP-2. 05 AUX-X USAGE IS COMP-2. 05 AUX-Y USAGE IS COMP-2. 05 AUX-Z USAGE IS COMP-2. LINKAGE SECTION. * Data to share with COBOL subroutines 01 LOAN-PARAMS. 05 INPUT-MSG. 10 PRIN-AMT PIC S9(7) USAGE IS DISPLAY. 10 INT-RATE PIC S9(2)V9(2) USAGE IS DISPLAY. 10 TIMEYR PIC S9(2) USAGE IS DISPLAY. 05 OUTPUT-MSG. 10 PAYMENT PIC S9(7)V9(2) USAGE IS DISPLAY. 10 ERROR-MSG PIC X(20). PROCEDURE DIVISION USING BY REFERENCE LOAN-PARAMS. * code goes here! 000-MAIN. MOVE "N" TO WS-ERROR. * DISPLAY "PRIN-AMT: " PRIN-AMT. * DISPLAY "INT-RATE: " INT-RATE. * DISPLAY "TIMEYR: " TIMEYR. PERFORM 100-INIT. IF WS-ERROR = 'N' PERFORM 200-PROCESS END-IF. PERFORM 300-WRAPUP. 100-INIT. IF INT-RATE <= 0 MOVE WS-MSG10 TO ERROR-MSG MOVE 10 TO RETURN-CODE MOVE 'Y' TO WS-ERROR ELSE IF TIMEYR <= 0 MOVE WS-MSG12 TO ERROR-MSG MOVE 12 TO RETURN-CODE MOVE 'Y' TO WS-ERROR END-IF END-IF. 200-PROCESS. INITIALIZE AUX-VARS. COMPUTE MONTHLY-RATE = (INT-RATE / 12 / 100). COMPUTE AUX-X = ((1 + MONTHLY-RATE) ** (TIMEYR*12)). COMPUTE AUX-Y = AUX-X * MONTHLY-RATE. COMPUTE AUX-Z = (AUX-X - 1) / AUX-Y. COMPUTE PAYMENT = PRIN-AMT / AUX-Z. MOVE WS-MSG00 TO ERROR-MSG. MOVE 0 TO RETURN-CODE. * DISPLAY "PAYMENT: " PAYMENT. * DISPLAY "ERROR-MSG: " ERROR-MSG. 300-WRAPUP. GOBACK.

Compile ambos programas para generar una librería compartida (*.so, *dylib)

cobc -m bcouta.cbl loancalc.cbl

El controlador d8parti será el encargado de reemplazar al JES, a continuación se muestra una versión simplificada de dicho módulo, cree un fichero d8parti.go y copie el siguiente código.

package main /* #include <stdlib.h> #include <stdio.h> #include <string.h> #include <libcob.h> #cgo CFLAGS: -I/opt/homebrew/Cellar/gnucobol/3.2/include #cgo LDFLAGS: -L/opt/homebrew/Cellar/gnucobol/3.2/lib -lcob */ import "C" import ( "fmt" "log" "os" "time" "unsafe" "github.com/spf13/viper" ) type step struct { Stepname string `mapstructure:"stepname"` Exec exec Dd []dd } type exec struct { Pgm string `mapstructure:"pgm"` } type dd struct { Name string `mapstructure:"name"` Dsn string `mapstructure:"dsn"` Disp string `mapstructure:"disp"` Normaldisp string `mapstructure:"normaldisp"` Abnormaldisp string `mapstructure:"abnormaldisp"` } var Step *step func config() error { // Read yaml config file viper.SetConfigName("step") viper.SetConfigType("yaml") viper.AddConfigPath(".") if err := viper.ReadInConfig(); err != nil { return err } // Unmarshal yaml config file if err := viper.Unmarshal(&Step); err != nil { return err } // Create Symlink for i := 0; i < len(Step.Dd); i++ { err := os.Symlink(Step.Dd[i].Dsn, Step.Dd[i].Name) if err != nil { switch { case os.IsExist(err): // DDNAME already exist log.Printf("INFO: DDNAME=%s already exists. %s", Step.Dd[i].Name, err) case os.IsNotExist(err): // DDNAME invalid log.Printf("ERROR: DDNAME=%s invalid ddname. %s", Step.Dd[i].Name, err) return err default: log.Println(err) return err } } } return nil } func cobCall(p string) error { defer delSymlink() c_progName := C.CString(p) defer C.free(unsafe.Pointer(c_progName)) n := C.cob_resolve(c_progName) if n == nil { return fmt.Errorf("ERROR: Program %s not found", p) } else { log.Printf("INFO: PGM=%s started", p) r := C.cob_call_with_exception_check(c_progName, C.int(0), nil) rc := int(C.cob_last_exit_code()) err := C.GoString(C.cob_last_runtime_error()) switch int(r) { case 0: log.Printf("INFO: program %s exited with return-code: %v", p, rc) C.cob_tidy() case 1: log.Printf("INFO: program %s STOP RUN with return-code: %v", p, rc) case -1: return fmt.Errorf("ERROR: program %s exit with return-code: %v and error: %s", p, rc, err) case -2: return fmt.Errorf("FATAL: program %s exit with return-code: %v and error: %s", p, rc, err) case -3: return fmt.Errorf("ERROR: program %s signal handler exit with signal: %v and error: %s", p, rc, err) default: return fmt.Errorf("ERROR: program %s unexpected return exit code: %v and error: %s", p, rc, err) } return nil } } func delSymlink() { for i := 0; i < len(Step.Dd); i++ { err := os.Remove(Step.Dd[i].Name) if err != nil { log.Printf("INFO: DDNAME=%s does not exists. %s", Step.Dd[i].Name, err) } } } func main() { start := time.Now() // Initialize gnucobol C.cob_init(C.int(0), nil) log.Println("INFO: gnucobol initialized") // Load config file if err := config(); err != nil { log.Printf("ERROR: reading yaml config file. %s", err) os.Exit(12) } // Call COBOL program -> EXEC PGM defined in JCL if err := cobCall(Step.Exec.Pgm); err != nil { log.Println(err) os.Exit(12) } elapsed := time.Since(start) log.Printf("INFO: %s elapsed time %s", Step.Exec.Pgm, elapsed) }

Para ejecutar el programa COBOL batch de pruebas, simplemente abra una consola y ejecute lo siguiente:

go run d8parti.go

¿Como crear un fichero de entrada de ejemplo (infile)?

El formato del fichero de entrada es muy sencillo

01 WS-LOAN. 05 WS-ACC PIC X(10). 05 FILLER PIC X(1). 05 WS-AMT PIC 9(7). 05 FILLER PIC X(1). 05 WS-INT PIC 9(2)V9(2). 05 FILLER PIC X(1). 05 WS-YEAR PIC 9(2).

Consta de un número de cuenta (10 bytes), un importe (7 bytes), un tipo de interés (4 bytes con dos posiciones decimales) y un periodo en años (2 bytes). Los campos se delimitan mediante un separador (FILLER 1 byte) para facilitar la lectura del fichero de entrada.

Puede utilizar el siguiente programa de ejemplo para generar el fichero de entrada.

package main import ( "flag" "fmt" "math/rand" "os" "strconv" "time" ) var r1 *rand.Rand var ( rows = flag.Int("rows", 1000, "number of rows to generate") ) var ( file = flag.String("file", "test.txt", "input file name") ) func main() { flag.Parse() s1 := rand.NewSource(time.Now().UnixNano()) r1 = rand.New(s1) f, err := os.Create(*file) if err != nil { fmt.Println(err) return } for i := 0; i != *rows; i++ { output := account(i) + "-" + amount() + "-" + interest() + "-" + yearsPending() + "\n" _, err := f.WriteString(output) if err != nil { fmt.Println(err) f.Close() return } } err = f.Close() if err != nil { fmt.Println(err) return } } func account(id int) string { return "id:" + fmt.Sprintf("%07d", id+1) } func amount() string { min := 1000 max := 1000000 a := strconv.Itoa(r1.Intn(max-min+1) + min) for i := len(a); i != 7; i++ { a = "0" + a } return a } func interest() string { return "0450" } func yearsPending() string { min := 5 max := 25 y := strconv.Itoa(r1.Intn(max-min+1) + min) if len(y) < 2 { y = "0" + y } return y }

7 - COBOL to Go

Convierta el código COBOL a Go.

Los avances en IA Gen permiten vislumbrar un futuro en el que la conversión de código entre distintos lenguajes de programación pueda realizarse de manera automática y transparente.

Sin embargo, deben tenerse en cuenta las características del lenguaje COBOL para seleccionar una opción que permita reconocer el código convertido de forma que pueda seguir siendo mantenido por el equipo responsable.

Vamos a utilizar la rutina COBOL de ejemplo que calcula la cuota de un préstamo.

****************************************************************** * * Loan Calculator Subroutine * ========================== * * A sample program to demonstrate how to create a gRPC COBOL * microservice. * ****************************************************************** IDENTIFICATION DIVISION. PROGRAM-ID. loancalc. ENVIRONMENT DIVISION. CONFIGURATION SECTION. DATA DIVISION. FILE SECTION. WORKING-STORAGE SECTION. * Declare program variables 01 WS-MSG. 05 WS-ERROR PIC X(01). 05 WS-MSG00 PIC X(20) VALUE 'OK'. 05 WS-MSG10 PIC X(20) VALUE 'INVALID INT. RATE'. 05 WS-MSG12 PIC X(20) VALUE 'INVALID NUMBER YEARS'. 01 AUX-VARS. 05 MONTHLY-RATE USAGE IS COMP-2. 05 AUX-X USAGE IS COMP-2. 05 AUX-Y USAGE IS COMP-2. 05 AUX-Z USAGE IS COMP-2. LINKAGE SECTION. * Data to share with COBOL subroutines 01 LOAN-PARAMS. 05 INPUT-MSG. 10 PRIN-AMT PIC S9(7) USAGE IS DISPLAY. 10 INT-RATE PIC S9(2)V9(2) USAGE IS DISPLAY. 10 TIMEYR PIC S9(2) USAGE IS DISPLAY. 05 OUTPUT-MSG. 10 PAYMENT PIC S9(7)V9(2) USAGE IS DISPLAY. 10 ERROR-MSG PIC X(20). PROCEDURE DIVISION USING BY REFERENCE LOAN-PARAMS. * code goes here! 000-MAIN. MOVE "N" TO WS-ERROR. DISPLAY "PRIN-AMT: " PRIN-AMT. DISPLAY "INT-RATE: " INT-RATE. DISPLAY "TIMEYR: " TIMEYR. PERFORM 100-INIT. IF WS-ERROR = 'N' PERFORM 200-PROCESS END-IF. PERFORM 300-WRAPUP. 100-INIT. IF INT-RATE <= 0 MOVE WS-MSG10 TO ERROR-MSG MOVE 10 TO RETURN-CODE MOVE 'Y' TO WS-ERROR ELSE IF TIMEYR <= 0 MOVE WS-MSG12 TO ERROR-MSG MOVE 12 TO RETURN-CODE MOVE 'Y' TO WS-ERROR END-IF END-IF. 200-PROCESS. INITIALIZE AUX-VARS. COMPUTE MONTHLY-RATE = (INT-RATE / 12 / 100). COMPUTE AUX-X = ((1 + MONTHLY-RATE) ** (TIMEYR*12)). COMPUTE AUX-Y = AUX-X * MONTHLY-RATE. COMPUTE AUX-Z = (AUX-X - 1) / AUX-Y. COMPUTE PAYMENT = PRIN-AMT / AUX-Z. MOVE WS-MSG00 TO ERROR-MSG. MOVE 0 TO RETURN-CODE. DISPLAY "PAYMENT: " PAYMENT. DISPLAY "ERROR-MSG: " ERROR-MSG. 300-WRAPUP. GOBACK.

Una primera aproximación pasa por mantener la estructura del código COBOL:

  • Una subrutina COBOL equivale a una función en Go
  • Las variables definidas en la WORKING-STORAGE se agrupan y transforman en variables Go
  • El código de la PROCEDUCE DIVISION se compone de uno o varias secciones (PARAGRAPHS), estas se pueden transformar a su vez en funciones muy sencillas
  • Por último, las variables de la LINKAGE SECTION definen los parámetros de la función principal y se comparten (puntero) entre todas las funciones
// Declare variables in the working storage section var ( WS_ERROR string WS_MSG00 string = "OK" WS_MSG10 string = "INVALID INT. RATE" WS_MSG12 string = "INVALID NUMBER YEARS" MONTHLY_RATE float64 AUX_X float64 AUX_Y float64 AUX_Z float64 ) // Data to share with COBOL subroutines type LoanParams struct { PrinAmt float64 IntRate float64 TimeYr int32 Payment float64 ErrorMsg string } func loancalc(amount float64, interest float64, nyears int32) (payment float64, errmsg string) { WS_ERROR = "N" loanParams := LoanParams{ PrinAmt: amount, IntRate: interest, TimeYr: nyears, } fmt.Println("PRIN-AMT:", loanParams.PrinAmt) fmt.Println("INT-RATE:", loanParams.IntRate) fmt.Println("TIMEYR:", loanParams.TimeYr) initial(&loanParams) if WS_ERROR == "N" { process(&loanParams) } wrapup(&loanParams) return loanParams.Payment, loanParams.ErrorMsg } func initial(loanParams *LoanParams) { if loanParams.IntRate <= 0 { loanParams.ErrorMsg = WS_MSG10 WS_ERROR = "Y" } else { if loanParams.TimeYr <= 0 { loanParams.ErrorMsg = WS_MSG12 WS_ERROR = "Y" } } } func process(loanParams *LoanParams) { MONTHLY_RATE = loanParams.IntRate / 12 / 100 AUX_X = math.Pow((1 + MONTHLY_RATE), float64(loanParams.TimeYr*12)) AUX_Y = AUX_X * MONTHLY_RATE AUX_Z = (AUX_X - 1) / AUX_Y loanParams.Payment = loanParams.PrinAmt / AUX_Z loanParams.ErrorMsg = WS_MSG00 } func wrapup(loanParams *LoanParams) { fmt.Println("PAYMENT:", loanParams.Payment) fmt.Println("ERROR-MSG:", loanParams.ErrorMsg) }

Hay que recordar que el código COBOL ya ha sido expuesto mediante una interfaz estándar que define los parámetros de entrada/salida de la función (por ejemplo, mediante un mensaje proto).

Utilizando la definición de dicha interfaz, es posible volver a refactorizar el código simplificando el resultado final.

func loancalc(amount, interest float64, nyears int32) (payment float64, errmsg string) { if interest <= 0 { return 0, "Invalid int. rate" } if nyears <= 0 { return 0, "Invalid number of years" } monthlyRate := (interest / 12 / 100) x := math.Pow((1 + monthlyRate), float64(nyears*12)) y := x * monthlyRate payment = amount / ((x - 1) / y) return payment, "OK" }

8 - Python

Desea utilizar Python

La tecnología gRPC nos permite conectar programas escritos en distintos lenguajes de programación de manera sencilla.

En este ejemplo, crearemos un cliente Python para llamar a nuestro servicio gRPC COBOL (hello.cbl).

Para ello primero necesitaremos compilar el mensaje proto para el lenguaje Python.

syntax = "proto3"; option go_package = "github.com/driver8soft/examples/d8grpc/hello"; package hello; // d8grpc hello service definition. service D8grpc { // Sends a greeting rpc Hello (MsgReq) returns (MsgRes) {} } // The request message containing the user's name. message MsgReq { string hello_name = 1; } // The response message containing the greetings message MsgRes { string response = 1; }

Instale el compilador correspondiente al lenguaje Python y ejecute el siguiente comando

python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. hello.proto

La compilación del fichero proto creará los stubs necesarios para nuestro cliente Python

  • hello_pb2.py
  • hello_pb2_grpc.py

A continuación crearemos un cliente Python gRPC, cree un fichero client.py y copie el siguiente código.

import grpc import hello_pb2 import hello_pb2_grpc def run(inputname): with grpc.insecure_channel('localhost:50051') as channel: stub = hello_pb2_grpc.D8grpcStub(channel) r = stub.Hello(hello_pb2.MsgReq(hello_name=inputname)) print(f"Result: {r.response}") if __name__ == '__main__': # Get user Input inputname = input("Please enter name: ") run(inputname)

Para probar el nuevo cliente Python, abra un terminal y ejecute

python client.py