Введение

Если вам когда-нибудь доводилось писать скрипты для Тарантула, то вы наверняка сможете понять мою боль. Тарантул - удивительный инструмент, который позволяет не только хранить относительно большие объёмы данных и обеспечивать поразительно быстрое выполнение операций 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. То есть, помимо одной документации Тарантула, вам теперь нужно дополнительно изучить две другие документации. Отличный результат проделанной работы, я считаю.

Но если серьёзно, то да - вместо того чтобы почти наугад писать на 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. Вторая цель определённо достигнута, а степень достижения первой цели будет возможно оценить только по прошествии какого-то времени.

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

Ссылки

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

Комментарии (6)


  1. 16tomatotonns
    26.04.2024 02:36

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

    Вообще, подобные вещи можно решить несколько проще:
    1. Второй монитор с документацией на удивление творит чудеса, очень советую : )
    2. Динамические системы автодополнения. Надо, правда, постараться, но когда набираешь httpd. - оно сразу может выводить ряд подсказок включая описания, аргументы и даже примеры, правда, для ООП может потребоваться определенное соглашение для именования этих объектов, чтобы подсказки работали по самому тексту.


    1. olivera507224 Автор
      26.04.2024 02:36

      Второй монитор с документацией на удивление творит чудеса, очень советую : )

      Подозреваю, что этот пункт просто ради рофла, иначе мне придётся выяснять, как монитор переносить в рюкзаке :)

      Динамические системы автодополнения.

      Да, VSCode умеет запоминать введённые слова и в дальнейшем предлагать их для автодополнения. Но всё же это не то же самое, что ввести имя переменной, точку и в выпавшем списке просматривать доступные методы и свойства. А хотелось именно этого.


  1. nikolz
    26.04.2024 02:36

    В качестве альтернативы:

    Вот так можно создать http сервер на Lua без тарантула:

    local server = require 'http.server'
    local headers = require 'http.headers'
    
    local srv = server.listen {
      host = 'localhost',
      port = 82,
      onstream = function (sv, out)
        local hdrs = out:get_headers()
        local method = hdrs:get(':method')
        local path = hdrs:get(':path') or '/'
    
        local rh = headers.new()
        rh:append(':status','200')
        rh:append('content-type','text/plain')
    
        out:write_headers(rh, false)
        out:write_chunk(("Received '%s' request on '%s' at %s\n"):format(method, path, os.date()), true)
      end
    }
    
    srv:listen()
    srv:loop()

    https://onelinerhub.com/lua/how-to-create-http-server

    --------------------------

    Для статической типизации можно использовать скриптовый язык Terra.

    Как и C/C++, язык Terra статически типизируемый, компилируемый язык с «ручным» управлением памятью. В отличие от C/C++, он изначально спроектирован для метапрограммирования с помощью Lua.

    https://habr.com/ru/articles/336406/


  1. starfair
    26.04.2024 02:36

    Я бы не стал утверждать, что Lua изучается за один вечер. Да, основы какие то можно понять. Но что то посерьезнее, типа работы с Global и метаобъектов, это далеко не такие простые темы.


  1. alex_tulski
    26.04.2024 02:36
    +1

    Спасибо за статью, очень интересно, надо попробовать.

    Поделюсь также нашим опытом. Для типизации мы используем плагин EmmyLua(есть и для JetBrains и для VSCode) и его аннотации.
    Чтобы подсветка синтаксиса для box и прочих тарантуловских объектов работала, мы делаем отдельный файлик с аннотациями. В нём описываем известные нам классы и методы работы с ними. Вот кусочек с примером:

    --- @class box
    --- @field once fun(key: string, func:fun(), ...):void Execute a function, provided it has not been executed before. A passed value is checked to see whether the function has already been executed. If it has been executed before, nothing happens. If it has not been executed before, the function is invoked.
    --- @field error fun(code_or_obj: number|{code:number, reason:string}, errtext:string, ...):void
    --- @field space space_object[]
    --- @field schema schema
    --- @field atomic fun(func: fun(), ...) Execute a function, acting as if the function starts with an implicit box.begin() and ends with an implicit box.commit() if successful, or ends with an implicit box.rollback() if there is an error.
    --- @field index index_object[]
    box = {}
    
    --- @class schema
    --- @field space space
    
    --- @class space
    --- @field create fun(name:string, options:{engine: string, field_count: number, format: table, id: number, if_not_exists:boolean, is_local:boolean, is_sync:boolean, temporary:boolean, user:string}):space_object
    


    1. olivera507224 Автор
      26.04.2024 02:36

      Да, знаю про такой, спасибо за дополнение. Именно реализация аннотаций от EmmyLua была взята за основу в языковом сервере для VSCode. Кстати, вы можете сделать доброе дело и поделиться вашими наработками, опубликовав плагин с типами Тарантула, подробности в этой доке ;)