You will learn and implement a microservice in COBOL without Mainframe. You will structure the project, manage dependencies, implement automatic tests and build virtualized execution environment. Finally, you will publish the microservice on GitHub under Continuous Integration workflow.
Preconditions
You have learned basic principles, methods and standards of COBOL. In this tutorial we’ll use GnuCOBOL — a free COBOL compiler which implements a substantial part of the COBOL 85, COBOL 2002 and COBOL 2014 standards and X/Open COBOL, as well as many extensions included in other COBOL compilers.
You are familiar with HTTP protocol — request and response formats.
You have Docker, a command-line virtualization tool, installed.
You have NPM, a package manager for JavaScript programming language, installed.
You have Git, an open source distributed version control client, installed.
You have GitHub account for publishing of the microservice.
You may use any text editor you like, but I recommend Visual Studio Code (or its open-source version VSCodium) with COBOL-syntax extension bitlang.cobol installed.
TLDR
Complete source-code of this tutorial you can see on GitHub.
Specifications
One of strengths of COBOL is a decimal mathematics. In this tutorial we’ll create a high-precision currency exchange microservice that exposes HTTP API and returns EUR amount in JSON format.
Let’s say, the microservice awaits HTTP request GET /currency/amount on port 8000 and respond JSON {"amount": amount}, where
- currency is a tree-letter ISO currency code, i.e. USD
- amount is a numeric value separated by dot, i.e. 999.999
Any mismatching requests, unsupported currencies, as well as calculation errors will result in 404 Not Found responses.
Exchange rates are Euro foreign exchange reference rates published by the European Central Bank. Daily renewals of the exchange rates and multi-threading are out of scope in this tutorial.
Structure
We need 3 directories — src for main program, tests for test program and resources for static files. Please download CSV (.zip) from ECB website and extract to resources directory. The file contains exchange rates to Euro for 32 currencies. As defined in the specification, we’ll keep the rates static.
$ ls
resources src tests
$ ls resources
eurofxref.csv
Finally, please create empty microservice.cbl and microservice-test.cbl files in src and tests directories respectively. We will need them later.
Dependencies
Our microservice depends on HTTP server for handling requests, ECB parser for CSV and GCBLUnit testing framework. All these components are available on COBOL Package Registry — cobolget.com. We can simply integrate these dependencies by using an open-source COBOL package management tool — cobolget. Here’s complete listing:
$ npm install -g cobolget
$ cobolget init
Manifest modules.json created.
$ cobolget add core-network
Dependency 'core-network' has been added to the manifest.
$ cobolget add core-string
Dependency 'core-string' has been added to the manifest.
$ cobolget add --debug gcblunit
Debug dependency 'gcblunit' has been added to the manifest.
$ cobolget update
Lockfile modules-lock.json updated.
$ cobolget -t bca12d6c4efed0627c87f2e576b72bdb5ab88e34 install
We use Team Token in the last command because core-network is private package owned by Cobolget but freely shared with the community. You will see long installation log which ends with
Modules modules.cpy and modules.cbl updated.
The file modules.cpy, already known as COBOL Copybook, includes all direct and inherited dependencies for the microservice. We’ll use it inside our program in the next step.
Program
Basically, our program must
- read CSV file
- convert CSV text into the list of Currency-Rate pairs
- launch local TCP/IP server on port 8000 by
- implementing a callback which handles HTTP requests
identification division.
program-id. microservice.
...
procedure division.
*> read CSV file into csv-content
open input file-csv.
if not file-exists
display "Error reading file" upon syserr
stop run
end-if.
perform until exit
read file-csv at end exit perform end-read
end-perform.
close file-csv. *> convert csv-content to the list of key-value pairs
move csv-ecb-rates(csv-content) to dataset.*> start HTTP server with http-handler callback
call "receive-tcp" using "localhost", 8000, 0, address of entry "http-handler".
end program microservice.identification division.
program-id. http-handler.
...
procedure division using l-buffer, l-length returning omitted.
*> initialize exchange rates
set address of exchange-rates to dataset-ptr.
*> parse request as "GET /<currency>/<amount>"
unstring l-buffer(1:l-length) delimited by all SPACES into request-method, request-path. if not http-get
perform response-NOK
end-if. *> find currency and calculate eur-amount
perform varying idx from 1 by 1 until idx > 64
if rate-currency(idx) = get-currency
compute eur-amount = numval(get-amount) / rate-value(idx)
on size error perform response-NOK
end-compute
perform response-OK
end-if
end-perform. *> or nothing
perform response-NOK.response-OK section.
move HTTP-OK to response-status.
move byte-length(response-content) to response-content-length.
perform response-any.response-NOK section.
move HTTP-NOT-FOUND to response-status.
move 0 to response-content-length.
perform response-any.response-any section.
move 1 to l-length.
string response delimited by size into l-buffer with pointer l-length.
subtract 1 from l-length.
goback.
end program http-handler.copy "modules/modules.cpy".
The receive-tcp program is a server which accepts incoming connections, reads the content of the request into the buffer and shares the buffer with the callback program. The callback parses the content and replaces the buffer with a response. The server sends the response back to the client. Full listing of the program you can find on GitHub.
Let’s install GnuCOBOL Docker execution environment.
$ docker run -d -i --name gnucobol olegkunitsyn/gnucobol:2.2
$ docker exec -i gnucobol cobc -V
cobc (GnuCOBOL) 2.2.0
Copyright (C) 2017 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Keisuke Nishida, Roger While, Ron Norman, Simon Sobisch, Edward Hart
Built Jul 26 2020 07:44:23
Packaged Sep 06 2017 18:45:29 UTC
C version "9.3.0"
In this tutorial we’ll use GnuCOBOL 2.2, only stable GnuCOBOL compiler available in the binary distributions at the moment. You may find and install it natively on your machine, too.
Test
Our microservice will follow Continuous Integration practices, when the developers integrate source-code into the shared repository, where each integration is getting tested automatically. For testing we’ll use simple GCBLUnit testing framework which was already installed as a debug-dependency earlier.
Let’s create Dockerfile of the microservice:
FROM olegkunitsyn/gnucobol:2.2
RUN mkdir /microservice
WORKDIR /microservice
COPY . .
EXPOSE 8000
RUN cobc -x -debug modules/gcblunit/gcblunit.cbl tests/* --job='microservice-test'
We expose port 8000 and execute microservice-test job upon each build of the image. The last element of the whole picture is a test-file microservice-test.cbl:
>>SOURCE FORMAT FREE
identification division.
program-id. microservice-test.
environment division.
configuration section.
repository.
function csv-ecb-rates
function substr-pos
function all intrinsic.
data division.
working-storage section.
01 dataset external.
05 dataset-ptr usage pointer.
01 buffer pic x(1024) value "GET /USD/1 HTTP1.1".
procedure division.
move csv-ecb-rates(concatenate("Date, USD, " x"0a" "17 July 2020, 1.1428, ")) to dataset.
call "http-handler" using buffer, byte-length(buffer).
perform http-handler-test.
goback.http-handler-test section.
call "assert-notequals" using 0, substr-pos(buffer, "HTTP/1.1 200 OK").
call "assert-notequals" using 0, substr-pos(buffer, "Content-Type: application/json").
call "assert-notequals" using 0, substr-pos(buffer, "Content-Length: 44").
call "assert-equals" using 104, substr-pos(buffer, "0.8750437521876093").
end program microservice-test.copy "src/microservice.cbl".
For testing purposes I’ve prepared minimal CSV content with one single currency USD. As you can see in definition of the buffer, the test requests the conversion of 1 USD. We expect non-nullable HTTP headers, as well as high-precision exchanged amount 0.8750437521876093. The last line includes the main program we test to.
Container
Let’s create Docker image:
$ docker build --tag microservice .
...
OK
Tests: 0000000001, Skipped: 0000000000
Assertions: 0000000004, Failures: 0000000000, Exceptions: 0000000000
...
Well done! Our Docker image successfully passed the test by evaluating 4 assertions and ready for launch.
$ docker run -d -i --name microservice -p 8000:8000 microservice
$ docker exec -i microservice cobc -j -x src/microservice.cbl
TCP server started on localhost:08000. Hit Ctrl+C to stop.
Open http://localhost:8000/USD/99.99 and http://localhost:8000/ABC/1 in the browser and see what happens. To stop and remove the container, run
$ docker rm --force microservice
GitHub
At last, we’ll publish the microservice enabling GitHub Actions workflow, where each pull request or push to the repository triggers an execution of microservice-test. All you need is docker-image.yml file in .github/workflows directory:
name: Docker Image CIon:
push:
branches: [ master ]
pull_request:
branches: [ master ]jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build the Docker image
run: docker build . --file Dockerfile --tag my-image-name:$(date +%s)
Conclusion
You have implemented the microservice by using Git libraries, package management, unit-testing and virtualization together with Continuous Integration approach. 60-years old COBOL fits modern software engineering.
Please contact me if you have any corrections or feedback.