COBOL

Предварительные условия

Вы знаете основные принципы, методы и стандарты COBOL. В этом руководстве мы используем GnuCOBOL — бесплатный компилятор COBOL, реализующий значительную часть стандартов COBOL 85, COBOL 2002, COBOL 2014 и X/Open COBOL, а также многие расширения, включённые в другие компиляторы COBOL.

Вы знакомы с протоколом HTTP  —  форматами запросов и ответов.

У вас установлены: 

  • Docker  —  инструмент виртуализации, работающий из командной строки.
  • NPM  —  менеджер пакетов для JavaScript.
  •  Git  —  клиент с открытым исходным кодом для управления версиями.

У вас есть аккаунт на GitHub для публикации микросервисов.

Вы можете использовать любой удобный текстовый редактор, но я рекомендую Visual Studio Code (или его версию с открытым исходным кодом VSCodium) с установленным расширением синтаксиса COBOL bitlang.cobol.

TLDR

Полный исходный код этого руководства на GitHub.

Спецификации

Одна из сильных сторон COBOL — это десятичные вычисления. В этом руководстве мы создадим высокоточный микросервис обмена валют, обрабатывающий HTTP API и возвращающий сумму в евро в формате JSON.

Скажем, микросервис ожидает HTTP запрос GET /<currency>/<amount> по порту 8000 и отвечает JSON {"amount": <amount>}, где:

  • <currency>  —  это трёхбуквенный ISO код валюты, например, USD;
  • <amount>  —  это числовое значение, разделённое точкой, например, 999.999

Любые несоответствующие запросы, неподдерживаемые валюты и ошибки вычисления приведут к ответу 404 Not Found.

Обменные курсы — это курсы евро, опубликованные Европейским центральным банком. Ежедневные обновления курса и поддержка многопоточности выходят за рамки этого руководства.

Структура

Нам нужно 3 каталога — src для основной программы, tests для тестовой и resources для статических файлов. Пожалуйста, скачайте CSV (.zip) с ECB и разархивируйте в каталог resources. Файл содержит обменные курсы евро к 32 валютам. Как указано в спецификации, курсы остаются неизменными.

$ ls
resources  src  tests
$ ls resources
eurofxref.csv

Наконец, создадим пустые файлы microservice.cbl и microservice-test.cbl в каталогах src и tests соответственно. Они понадобятся нам позже.

Зависимости

Наш микросервис зависит от: 

Все эти компоненты доступны в реестре пакетов COBOL — cobolget.com. Мы с лёгкостью можем интегрировать эти зависимости с помощью инструмента управления пакетами COBOL с открытым исходным кодом cobolget. Вот полный листинг:

$ 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

В последней команде используется Team Token, поскольку core-network является закрытым пакетом, принадлежащим Cobolget,но свободно распространяемым в сообществе. Вы увидите длинный процесс загрузки, заканчивающийся строкой:

Copybook modules.cpy updated

Этот файл, уже известный как COBOL Copybook, включает в себя все прямые и унаследованные зависимости для микросервиса. Мы используем его внутри нашей программы на следующем этапе.

Программа

По сути наша программа должна:

  • читать CSV-файл;
  • преобразовывать CSV-текст в список пар валюта-курс;
  • запускать локальный TCP/IP сервер на 8000 порту с помощью реализации обратного вызова, который обрабатывает HTTP запросы.
identification division.
program-id. microservice.
...
procedure division.
  *> чтение CSV-файла в csv-содержимое
  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.

  *> преобразование csv-содержимого в список пар ключ-значение
  move csv-ecb-rates(csv-content) to dataset.

*> запуск HTTP сервера с обратным вызовов http-обработчика
  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.
  *> инициализация обменного курса
  set address of exchange-rates to dataset-ptr.
  
  *> парсинг запроса как "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.

  *> поиск валюты и расчёт суммы в евро
  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.

  *> или ничего
  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".

Программа receive-tcp  —  это сервер, который принимает входящие соединения, считывает содержимое запроса в буфер и делится буфером с программой обратного вызова. Обратный вызов парсит содержимое и заменяет буфер ответом. Сервер отправляет ответ обратно клиенту. Полный листинг программы на GitHub.

Давайте установим среду выполнения GnuCOBOL Docker:

$ 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"

В этом руководстве мы используем GnuCOBOL 2.2, единственный стабильный GnuCOBOL компилятор, доступный в настоящее время в бинарных дистрибутивах. Вы так же можете найти его и установить в исходном формате на свой компьютер.

Тест

Наш микросервис будет следовать практике непрерывной интеграции, когда разработчики интегрируют исходный код в общий репозиторий, где каждая интеграция тестируется автоматически. Для тестирования воспользуемся простой тестовой средой GCBLUnit, уже установленной ранее как зависимость для отладки.

Давайте создадим Dockerfile микросервиса:

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'

Мы открываем 8000 порт и выполняем работу microservice-test при каждой сборке образа. Последний элемент всей картины — это тест-файл 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".

Для целей тестирования я подготовил минимальное CSV содержимое только с одной валютой — USD. Как видно из определения буфера, тест запрашивает конвертацию 1 USD. Мы ожидаем ненулевые HTTP-заголовки, а также обменную сумму с высокой точностью 0.8750437521876093. Последняя строка включает в себя основную тестируемую программу.

Контейнер

Создадим образ Docker:

$ docker build --tag microservice .
...
OK
Tests: 0000000001, Skipped: 0000000000
Assertions: 0000000004, Failures: 0000000000, Exceptions: 0000000000
...

Прекрасно! Наш образ Docker успешно прошёл тест, рассчитав 4 выражения, и готов к запуску:

$ 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.

Откроем http://localhost:8000/USD/99.99 и http://localhost:8000/ABC/1 в браузере и посмотрим, что произойдёт. Чтобы остановить и удалить контейнер, запустим:

$ docker rm --force microservice

GitHub

Наконец, опубликуем микросервис, применяя GitHub Actions, где каждый запрос на включение внесённых изменений или пуш в репозиторий запускают выполнение microservice-test. Всё, что вам нужно,  —  это файл docker-image.yml в каталоге .github/workflows:

name: Docker Image CI

on:
  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)

Заключение

Мы реализовали микросервис с помощью библиотеки Git, менеджера пакетов, модульного тестирования и виртуализации в рамках подхода непрерывной интеграции. COBOL, которому уже 60 лет, подходит для современной разработки программного обеспечения!

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Oleg Kunitsyn: Modern COBOL: Microservice Tutorial

Предыдущая статьяЭлементы управления выбором в пользовательском интерфейсе
Следующая статьяДиагностика кода на Rust