2017-09-21 22:49:00 Dev

Первый опыт с Go. Печать pdf-документов через API

Вот и я наконец решил попробовать свои силы в изучении языка Go. До последнего момента он мне казался очередным хипстерским, новомодным языком, который толком нигде нельзя использовать. Зачем Гугл придумал новый язык конечно было понятно сразу (потому что может), но чем он может заинтересовать простых смертных на тот момент я не особо понимал. По этой причине, долгое время я не относился к нему серьезно. Я слышал о его быстродействии, многопоточности "из коробки" и  простоте, но все это меня не смогло заинтересовать настолько, чтобы попробовать Go. Однако, последнее время стало появляться большое количество различных туториалов по Go, статей описывающих опыт применения Go в продакшене от крупных компаний - Avito, MailRu, HeadHunter и просто упоминании этого языка в сети.

Для меня решающим аргументом попробовать Go стала статья на habrahabr.ru где описывалось написание простого веб - сервера на Go. Веб - сервер поднимался в 3 строчки кода. Всего три строчки! После этого я решил - пора, и в тот же день установил язык себе на машину.

Еще одним немаловажным аргументом в пользу Go была компактность скомпилированной программы. Go поддерживает кросс компиляцию под любую платформу и в результате на выходе получается один исполняемый файл. Как следствие, такая компактность делает простой организацию процессов CI/CD.

Над решаемой приложением задачей долго думать не пришлось. Недавно вступивший в силу 54 федеральный закон натолкнул меня на мысль написать приложение для печати pdf файлов. На вход приложения поступает POST запрос с данными, приложение печатает данные в файл, кладет его в файловую систему, и отдает ссылку на него. Забегая вперед, скажу что приложение изначально задумывалось для печати pdf-чеков, но на данным момент оно умеет так же печатать произвольный текст с простой стилизацией.

Само по себе приложение осуществляет печать через пакет gofpdf. Запись файлов осуществляется в папку receipts в корне проекта.

Endpoint'ы и запуск приложения

Если коротко, то приложение имеет три основных и три дополнительных endpoint:

r.GET("/",Root)
r.GET("/info", Info)
r.GET("/healthz", Healthz)

r.POST("/v1/create", apiv1.CreateReceipt)
r.POST("/v1/createcustom", apiv1.CreateCustom)
r.GET("/v1/pdf/:docName", apiv1.GiveFile)
  1. /v1/create - Принимает POST запрос с данными заранее определенного формата. В ответ возвращается ссылка на напечатанный pdf-файл
  2. /v1/createcustom - Принимает POST запрос с произвольными данными. В ответ так же возвращается ссылка на напечатанный pdf-файл.
  3. /v1/pdf/:docName - Принимает Get запрос, где :docName - имя напечатаного pdf-файла. В ответ возвращается pdf-файл.
  4. /info - На Get запрос возвращает информацию о текущем состоянии приложения:
  5. 1. Имя хоста, его конфигурация (количество cpu)
  6. 2. Информация о рантайме - количество потребляемой приложением памяти, количество запущенных горутин (потоков)
  7. 3. Информация о билде - версия приложения, репозиторий, коммит.
  8. /healthz - Get запросом позволяет быстро понять пациент мертв или жив. В ответ возвращается http код. 200 - приложение живое.

Первые три эндпоинта предполагаются для клиенского использования, эндпоинты 4 и 5 - для систем оркестрации (Kubernetes).

Для запуска, приложение требует через переменную окружения PORT указать порт на котором оно будет работать:

$ export PORT=8081

Поле этого компилируем бинарник и запускаем набрав в терминале:

$ go build
$ ./rprint

Приложение запустится и поднимет сервер на порту 8081 по адресу http://localhost:8081

Печать

Как выше упоминалось, приложение может печатать как произвольный текст, так чек заранее определенного формата. Для печати чека в соседнем окне терминала отправляем следующий запрос:

$ curl -X POST -d '{"Schema": "default", "ReceiptS": {"MPlaceName": "Exmaple header", "MPlaceAddress": "www.example.com",  "MPlaceINN": "00000111111239990", "OperationType": "Sell", "Items": [{"Name": "Raincoat", "Quantity": 1.000, "Price": 100.0}, {"Name": "Black Hat", "Quantity": 1.000, "Price": 33.0}, {"Name": "Gloves", "Quantity": 1.000, "Price": 15.0}], "TaxPercent": "18%", "Total": 148.0, "FiscalNumber": "000000000011198", "Date": "2017-06-11 23:21:11"}}' http://localhost:8081/v1/create

В ответ вернется json-объект содержащий ссылку на напечатанный файл:

{"link":"http://localhost:8081/v1/pdf/1496491926257726883"}

Открыв ссылку в браузере, будет отображен  pdf-документ с чеком:

 

В теле запроса на печать чека передается json-объект определенной структуры:

  • MPlaceName - имя организации осуществляющей продажу (string)
  • MPlaceAddress -адрес организации (string)
  • MPlaceINN - ИНН организации (string)
  • OperationType - тип операции (string)
  • Items - массив из продаваемых позиций (array)
  • Каждая позиция состоит из:
  • - Name - название позиции (string)
  • - Quantity - количество товара (float)
  • - Price - стоимость за единицу товара (float)
  • TaxPercent - размер налога (string)
  • Total - общая стоимость чека (float)
  • FiscalNumber - фискальный номер чека (string)
  • Date - дата покупки (string)

Печать произвольного текста, абстрактно, представляет собой пошаговое выполнение инструкций. Инструкции могут быть двух типов "text" - собственно печать текста, "nl" - печать пустых строк.

Печать текста осуществляется построчно, то есть одна инструкция - одна строка. Пример запроса для печати произвольного текста:

$ curl -X POST -d '{"PageConfig": {"Orientation": "P",  "Format": "A4",  "FontStyle": "I"}, "Instructions": [{"Type": "text", "Value": "www.example.com", "LineConfig": {"FontSize": 16.0, "Width": 0, "Height": 7.0, "NewLine": 1, "Align": "C"}}, {"Type": "text", "Value": "1. Apple", "LineConfig": {"FontSize": 16.0, "Width": 5.0, "Height": 7.0, "NewLine": 0, "Align": "L"}}, {"Type": "text", "Value": "1 pc", "LineConfig": {"FontSize": 16.0, "Width": 150.0, "Height": 7.0, "NewLine": 0, "Align": "C"}}, { "Type": "text", "Value": "5.00 $", "LineConfig": {"FontSize": 16.0, "Width": 0, "Height": 7.0, "NewLine": 1, "Align": "R"}}, {"Type": "text", "Value": "2. Chocolate", "LineConfig": {"FontSize": 16.0, "Width": 5.0, "Height": 7.0, "NewLine": 0, "Align": "L"}}, {"Type": "text", "Value": "2 pc", "LineConfig": {"FontSize": 16.0, "Width": 150.0, "Height": 7.0, "NewLine": 0, "Align": "C"}}, {"Type": "text", "Value": "10.30 $", "LineConfig": {"FontSize": 16.0, "Width": 0, "Height": 7.0, "NewLine": 1, "Align": "R"}}, {"Type": "nl", "Value": "3", "LineConfig": {"FontSize": 0, "Width": 0, "Height": 0, "NewLine": 0, "Align": ""}}, {"Type": "text", "Value": "TOTAL: ", "LineConfig": {"FontSize": 16.0, "Width": 0, "Height": 7.0, "NewLine": 0, "Align": "L"}}, {"Type": "text",   "Value": "15.30 $", "LineConfig": {"FontSize": 16.0, "Width": 0, "Height": 7.0, "NewLine": 1, "Align": "R"}} ]}' http://localhost:8081/v1/createcustom

В ответ так же придет ссылка на напечатанный pdf-файл.

Тело запроса на печать произвольного текста имеет следующий формат:

  • PageConfig - объект определяющий глобальные настройки печати.
  • - Orientation - Ориентация страницы. По умолчанию - "P". Возможные значения:
    • = "P" или "Portrait" - портретная ориентация страницы
    • = "L" или "Landscape" - ландшафтная ориентация страницы
  • Format - формат листа. Возможные значения: "A3", "A4", "A5", "Letter", "Legal".
  • FontStyle - стиль текста. Возможные значения: "B" (bold), "I" (italic), "U" (underscore) или любые их комбинации.
  • Instructions - массив из инструкций для печати.
    • - Type - тип инструкции. Возможные значения:
      • = text - инструкция для печати текста
      • = nl - инструкция для печати новой строки
    • - Value - значение инструкции (текст)
    • - LineConfig - параметры определяющие настройки текущей печатаемой инструкции
      • - FontSize - размер шрифта
      • - Width - ширина печатаемой зоны
      • - Height - высота печатаемой зоны
      • - NewLine - выполнять ли переход на новую строку после печати текущей строки
        • = 0 - оставить каретку на текущей строке
        • = 1 - перевести каретку на новую строку
      • - Align - выравнивание текста в пределах текущей строки
        • = "L" - Выравнивание по левому краю
        • = "R" - Выравнивание по правому краю
        • = "C" - Выравнивание по центру

Примеры инструкций типа "text" и "nl":

// "Type": "text"
// "Value" - содержит печатаемую строку
{
	"Type": "text",
	"Value": "www.example.com",
	"LineConfig": {
		"FontSize": 16.0,
		"Width": 10.0,
		"Height": 14.0,
		"NewLine": 0,
		"Align": "L"
	}
}

// "Type": "nl"
// "Value" - содержит количество пустых строк
{
	"Type": "nl",
	"Value": "4",
	"LineConfig": {
		"FontSize": 0.0,
		"Width": 0.0,
		"Height": 0.0,
		"NewLine": 0,
		"Align": ""
	}
}

CI/CD магия

Для приложения настроена вся цепочка CI/CD с использованием Travis Ci, а это значит что приложение можно запустить прямо из Docker на лету скачав контейнер с ним из DockerHub:

$ docker run -p 8000:8081 --env PORT="8081" rtemb/rprint

Docker скачает нужный образ из registry и запустит приложение на 8000 порту хостовой машины.

На данный момент каждый коммит  репозиторий запускает цепочку CI/CD:

Commit -> GitHub -> Travis CI -> DockerHub -> Heroku

После того как Travis CI соберет билд, положить в контейнер и прогонит тесты, контейнер отправляется в DockerHub и далее разворачивается в облаке Heroku по адресу https://rprint.herokuapp.com. Поиграться с приложением можно отправив curl запрос:

$ curl -X POST -d '{"Schema": "default", "ReceiptS": {"MPlaceName": "Exmaple header", "MPlaceAddress": "www.example.com",  "MPlaceINN": "00000111111239990", "OperationType": "Sell", "Items": [{"Name": "Raincoat", "Quantity": 1.000, "Price": 100.0}, {"Name": "Black Hat", "Quantity": 1.000, "Price": 33.0}, {"Name": "Gloves", "Quantity": 1.000, "Price": 15.0}], "TaxPercent": "18%", "Total": 148.0, "FiscalNumber": "000000000011198", "Date": "2017-06-11 23:21:11"}}' https://rprint.herokuapp.com/v1/create

Просто перейдя по адресу приложения в Heroku, в браузере будет выведен текст: Processing URL /...

Заключение

В будущем есть планы совсем отвязать приложение от чеков и добавить что то вроде макросов позволяющих описывать внешний вид документа, а в самом запросе непосредственно отправлять только данные для печати. Таким образом приложение станет более универсальным, а POST запросы менее объемными.

Также, внимательный читатель конечно же заметит, что Docker контейнеры и файловая система вещи малосовместимые, и будет прав. Конечно же, приложение не предполагает долгого хранения распечатанного файла. Идея состояла в том, что получив ссылку на документ, клиентский код получит с ее помощью файл и сам решит что с ним делать дальше.  С таким подходом программа получается более гибкой. Но вместе с тем здесь есть еще над чем подумать. Возможно, в качестве фичи прикрутить механизм загрузки файлов, например в S3...

В целом, попробовав Go, я не разу не пожалел о потраченном на изучение языка времени. Этот язык идеально подходит для написания микросервисов. Готовая компактная программа легко докеризируется. Язык действительно очень простой и лаконичный, оставаясь вместе с тем достаточно низкоуровневым! Кросскомпиляция под любую платформу, многопоточность, производительность, отлично реализованные подходы конкурентного программирования средствами языка... что еще для счастья нужно?

Исходный код приложения можно найти здесь: https://github.com/rtemb/rprint