TarantoolScript: статическая типизация в Lua-скриптах Tarantool

Введение

Если вам когда-нибудь доводилось писать скрипты для Тарантула, то вы наверняка сможете понять мою боль. Тарантул — удивительный инструмент, который позволяет не только хранить относительно большие объёмы данных и обеспечивать поразительно быстрое выполнение операций CRUD над этими данными, но и предоставляет очень широкие возможности для обработки этих данных непосредственно в среде Тарантула. И под обработкой данных я имею ввиду не просто их валидацию и выполнение над ними каких-то математических операций, а почти весь спектр возможностей, предоставляемых языком Lua и ещё целую кучу полезных модулей, входящих в пакет поставки Тарантула или устанавливаемых из сторонних источников.

Для того чтобы написать, например, полноценный HTTP-сервер на Тарантуле (прошу не пинать меня за эту формулировку), нам нужно знать совсем немного — основы синтаксиса языка Lua и API основных модулей самого Тарантула. И вот если с Lua всё совсем просто — изучить этот язык за один вечер, я уверен, мало для кого окажется непосильной задачей — то вот с модулями Тарантула всё немного сложнее. Можно вдоль и поперёк проштудировать всю официальную документацию и уже непосредственно во время написания скрипта столкнуться с одной неприятной проблемой — писать относительно большие вещи для Тарантула жутко неудобно.

Проблема

И вот в чем заключается моя боль. Я всё пишу в VSCode и довольно нередко мне приходится писать скрипты для Тарантула. А это значит, что в процессе у меня нет привычных мне подсказок редактора или хоть какого-нибудь статического анализа типов. Нет, конечно, языковой сервер Lua предоставляет инструменты для работы непосредственно с исходным кодом Lua, но вот как только дело доходит до использования встроенных модулей Тарантула, почти весь связанный с ними код приходится писать наугад. Я не могу быть уверенным в том, в каких модулях какие находятся функции, какие у них сигнатуры, что они возвращают и как с ними работать. Зубрить документацию, конечно, не вариант (я уже итак себя пересилил и прочёл её, что вам ещё нужно?), а переключаться каждую секунду на окно браузера чтобы найти нужную информацию — такое себе, если честно.

Поиск решения

Однажды, сидя за ноутбуком и клацая по клавишам в процессе написания очередного Lua-скрипта, я пришёл к одной интересной мысли. А что, если существует инструмент, который позволит описать типы, предоставляемые Тарантулом, а затем описанное представление уже использовать в процессе разработки новых скриптов? Опыт JavaScript и TypeScript подсказывает, что решение этой проблемы — не самая плохая идея. Мысль на самом деле простая и очень странно, что она возникла у меня только спустя год после знакомства с Тарантулом. Быстрый гуглёж позволил мне найти три перспективных направления для работы над проблемой.

  1. LuaLS — расширение для VSCode с языковым сервером для Lua. Предоставляет возможность написать собственный аддон, описывающий требуемую схему типов. Собственно, этим расширением я итак пользуюсь, а идея добавить туда нужные мне элементы выглядит довольно интересной.

  2. Teal — диалект Lua со статической типизацией. Что-то вроде TypeScript, но для Lua. Также позволяет описать используемые типы, а затем скомпилировать программу в нативный Lua-скрипт. Выглядит также интересно.

  3. TypeScriptToLua (TSTL) -, а вот это уже самый настоящий TypeScript, который компилируется в Lua.

Возможно, есть ещё инструменты, решающие мою проблему, но дальше этих трёх я решил не ходить. Первые два выглядят довольно простыми в реализации, поэтому я подумал, что было бы неплохо заняться чем-то более полезным посложнее. Мой выбор пал на третий инструмент. Ну, а что, заодно и TypeScript подтяну.

Решение

Не буду тут тянуть кота долго и нудно описывать процесс написания своего пакета для TSTL, дабы не вызвать у читателя приступы зевоты, поэтому сразу представлю результат моих страданий трудов. TarantoolScript — пакет, предоставляющий объявления типов, которые могут быть полезны при написании скриптов для Тарантула. Следите за руками: мне очень нравится Lua, поэтому я сделал всё возможное, чтобы не писать на Lua:)

Для того чтобы эта штука завелась, вам, само собой, нужно иметь установленные пакеты TypeScript и TSTL. Ну и уметь в TypeScript, конечно же. Ну и немного посмотреть в документацию TSTL. То есть, помимо одной документации Тарантула, вам теперь нужно дополнительно изучить две другие документации. Отличный результат проделанной работы, я считаю.

a3d3f177912a6095b981e3ee55ff93ea.jpeg

Но если серьёзно, то да — вместо того чтобы почти наугад писать на Lua, пакет позволяет легко и почти безболезненно использовать практически все возможности TypeScript. О том, чтобы всё это скомпилировать в Lua, позаботится уже TSTL.

Примеры

Миграция базы данных, если это можно так назвать, конечно, может выглядеть как-то так:

{
        name: '_create_partners',
        up: function (this: Migration, userName: string): void {
            box.once(this.name, () => {
                const space = box.schema.space.create('partners', {
                    if_not_exists: true,
                    engine: 'memtx',
                    user: userName,
                });

                space.format([
                    { name: 'id', type: 'uuid' },
                    { name: 'name', type: 'string' },
                ]);

                space.create_index('primary', {
                    unique: true,
                    if_not_exists: true,
                    parts: ['id'],
                });

                box.space._schema.delete(`once${this.name}_down`);
            });
        },
        down: function (this: Migration): void {
            box.once(`${this.name}_down`, () => {
                box.space.get('partners').drop();
                box.space._schema.delete(`once${this.name}`);
            });
        },
    },

Вот как это будет скомпилировано:

{
        name = "_create_partners",
        up = function(self, userName)
            box.once(
                self.name,
                function()
                    local space = box.schema.space.create("partners", {if_not_exists = true, engine = "memtx", user = userName})
                    space:format({{name = "id", type = "uuid"}, {name = "name", type = "string"}})
                    space:create_index("primary", {unique = true, if_not_exists = true, parts = {"id"}})
                    box.space._schema:delete(("once" .. self.name) .. "_down")
                end
            )
        end,
        down = function(self)
            box.once(
                self.name .. "_down",
                function()
                    box.space.partners:drop()
                    box.space._schema:delete("once" .. self.name)
                end
            )
        end
    }

А вот так можно инициализировать HTTP-сервер:

import * as http_server from 'http.server';

const httpd = http_server.new_(host, port, {
    log_requests: true,
});

httpd.route({ path: '/sync/:partner_id/:partner_user_id', method: 'GET' }, handlerSync);
httpd.route({ path: '/match', method: 'GET' }, handlerMatch);
httpd.route({ path: '/partners', method: 'GET' }, handlerPartners);
httpd.route({ path: '/partners', method: 'POST' }, handlerPartners);
httpd.route({ path: '/partners/:partner_id', method: 'GET' }, handlerPartners);
httpd.route({ path: '/partners/:partner_id', method: 'DELETE' }, handlerPartners);
httpd.start();

TSTL превратит это в следующий кусок кода:

local http_server = require("http.server")
local httpd = http_server.new(host, port, {log_requests = true})
httpd:route({path = "/sync/:partner_id/:partner_user_id", method = "GET"}, handlerSync)
httpd:route({path = "/match", method = "GET"}, handlerMatch)
httpd:route({path = "/partners", method = "GET"}, handlerPartners)
httpd:route({path = "/partners", method = "POST"}, handlerPartners)
httpd:route({path = "/partners/:partner_id", method = "GET"}, handlerPartners)
httpd:route({path = "/partners/:partner_id", method = "DELETE"}, handlerPartners)
httpd:start()

И всё это с подсказками редактора и со статической проверкой типов. Сказка же!

Не сказка

Конечно же, есть проблемы. Сама TSTL, например, хоть и существует уже достаточно давно, но имеет ряд проблем. Достаточно только посмотреть на количество открытых issues в репозитории проекта (119 на момент написания статьи). Не очень корректно работают source maps, весьма скудные возможности управления импортом модулей, постоянная путаница с тем, нужно ли для функций явно указывать this или нет, и другое. Проект, конечно, далеко не заброшен, но и не то чтобы обновляется каждый день.

На что обратить внимание

Если читатель дошёл до этого момента, то статья наверняка его заинтересовала. Предположу, что ему также может быть интересно попробовать пощупать мою библиотеку собственными руками. Поэтому вот примерный список того, что нужно учитывать для работы с TarantoolScript в частности и с TSTL в общем:

  • Перед тем, как обратиться к модулю box, необходимо явно его объявить. То есть, где-то вверху файла с исходным кодом должен быть вот такой кусок:
    import { Box } from 'tarantoolscript';
    declare const box: Box;

  • Если захочется использовать какой-либо модуль помимо box, нужно внести небольшие изменения в tsconfig.json. Например, если есть необходимость подключить модуль log, то в конфиге обязательно должны быть следующие настройки:
    {
    "compilerOptions": {
    "paths": {
    "log": ["tarantoolscript/src/log.d.ts"]
    }
    },
    "tstl": {
    "noResolvePaths": [
    "log"
    ]
    }
    }

    Для luatest.helpers, например, нужно добавить:
    {
    "compilerOptions": {
    "paths": {
    "luatest.helpers": ["tarantoolscript/src/luatest.helpers.d.ts"] }
    },
    "tstl": {
    "noResolvePaths": [
    "luatest.helpers"
    ]
    }
    }

    После чего модули можно импортировать:
    import * as log from 'log';
    или
    import * as luatest_helpers from 'luatest.helpers';
    Для других модулей, если они уже имеют описания типов, всё то же самое — только поменять названия.

  • Для того чтобы иметь возможность корректно получать значения функций, возвращающих несколько значений за раз, следует использовать деструктуризацию:
    const [ok, body] = pcall(() => req.json());

В целом много полезного в плане синтаксиса можно почерпнуть в этом и этом проектах. Первый — это просто набор сэмплов, наглядно демонстрирующих как код TS компилируется в Lua (главное не забудьте про npm run build). Второй — пример проекта, который можно написать с использованием описываемых мной инструментов.

Заключение

Под конец статьи я вдруг вспомнил, что в самом её начале было бы неплохо описать цели проделанной мной работы. Но так как я не совсем нормальный переписывать мне лень, рискну сформулировать цели здесь.

Первая цель — это, конечно же, иметь удобный инструмент, который позволит облегчить мне жизнь. Вторая и, наверное, главная цель — это набраться опыта в Lua, TypeScript и Tarantool. Вторая цель определённо достигнута, а степень достижения первой цели будет возможно оценить только по прошествии какого-то времени.

Предположу, что самый банальный вопрос, который только может задать скептически настроенный читатель, выглядит примерно так: «А зачем это всё надо?» Мой же самый банальный ответ — так ведь это же интересно! Иначе зачем бы мы тогда тут все собирались?

Ссылки

Спасибо за внимание!

© Habrahabr.ru