Всем известно, что в Erlang нет привычных объектов из “классического” ООП, но они, по сути, и не нужны. Под словом объект, в данном случае, будет пониматься связка структура (record) и модуль, в котором описаны все функции для работы с этой структурой.
Приведу пример, который буду рассматривать на протяжении всех своих рассуждений:
Классический объект – User с полями login, email, password и salt (куда без нее). Его можно описать так (приставка db_ – необходима, т.к. в erlang уже есть модуль с таким именем).
1 2 3 4 5 6 7 8 9 10 11  |  | 
Я хочу описать два своих проекта tq_transform и tq_db. Которые суммарно генерируют объект позволяющий:
- Прятать реализацию, т.е. объекты за пределами модуля db_user не должны работать с record-ом напрямую. Это позволит в случае необходимости с легкостью поменять record, к примеру, на map.
 - Задавать значения по умолчанию.
 - Работать с “внешним миром”. Существует возможность указать какие поля и доступны для отправки внешнему пользователю ( к примеру через REST api ) и в каком виде, какие нет, какие только для чтения, какие только для записи.
 - Конвертировать содержимое полей в/из proplist.
 - Выводить список измененных полей.
 - Задавать валидаторы как отдельных полей, так и всей модели в целом.
 - Проводить загрузку модели в/из БД, притом представление полей в БД и Erlang может отличаться. 
- При обновлении модели, подставлять в SQL запрос только обновленные поля.
 - Задавать хуки на события: 
before_save,after_save,before_delete,after_delete 
 
tq_record_transform
Начну с того, что tq_record_transform – это базовый плагин tq_transform для генерации функций, назначение которых будет описано далее, по описанию заданному через атрибуты -field и -model. Сначала я постараюсь описать, как решается проблема в общем виде в Erlang, следом – как это реализовано или работает через tq_transform и почему именно так.
Прячем реализацию
Для того чтобы нормально работать с полями структуры не обращаясь к ним напрямую нужно задать getter-ы и setter-ы
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44  |  | 
Как можно заметить имя record-а совпадает с именем модуля. Я уже много раз писал об этом в том числе на habre. Это позволяет использовать объект как классическим способом
1 2 3 4 5  |  | 
так и более емким
1 2 3 4 5  |  | 
Теперь как то же делается с помощью tq_transform.
1 2 3 4 5 6 7 8  |  | 
Т.е. надо в модуль добавить указание компилятору использовать parse_transform. Далее определяются поля, имеющие отношение к данной модели через атрибут -field() и кортеж {FieldName, Opts}. FieldName – имя поля, а про Opts поговорим чуть далее.
tq_transform сгенерирует record, геттеры, сеттер и еще много вспомогательных функций.
Значения по умолчанию.
Для того чтобы задать default-ные значения для модели надо модифицировать метод new/2. В нашем примере это не требуется, но, к примеру, если бы у нас было поле group, которое указывало бы имя группы в какой находится пользователь и по умолчанию оно было бы равно “main”, то это бы выглядело так:
1 2 3 4 5 6 7 8  |  | 
В tq_record_transform этот же функционал достигается через опции. Их две: {default, Value} – задает значение по умолчанию. {default_fun, Fun} – значение, если его необходимо вычислить.
Примеры:
1 2 3 4  |  | 
или
1 2 3 4 5 6 7  |  | 
Сразу хочу рассказать о Fun. Везде, где в опциях упоминается функция, а это: deafult_func, валидаторы, хуки, Fun; может быть:
F::atom()– это равносильно простому вызову функции в том же модуле{F::atom(), Args::list()}– вызов F в текущем модуле с дополнительными аргументами Args (всегда идут вначале){M::atom(), F::atom()}– равносильно вызову F в модуле M{M::atom, F::atom(), Args::list()}– вызов F в модуле M с дополнительными аргументами Args.
Т.е. если get_default_group/0 значение по-умолчанию можно всеми ниже перечисленными способами:
1 2 3 4  |  | 
Функция определена таким образом в виду того, что в атрибуте нельзя сделать вызов или указать лямбду.
Ограничение доступа к полям.
Поля в модели могут иметь различные параметры доступа:
- разрешения для чтения/записи
 - разрешения для сериализации/десериализации
 
В текущем примере особыми параметрами отличаются поля password и salt. salt – никогда не видна конечному пользователю, она нужна только для внутренних проверок. password – можно только писать, т.к. при сохранении в БД он солится и хэшируется, то для клиента он более не несет никакой смысловой нагрузки.
В обычной версии пока нечего писать, т.к. еще не определены функции для сериализации/десериализации, но к ним еще вернемся в следующем пункте.
В tq_record_transform существует опция {mode, AccessMode}, которая отвечает за все правила доступа к полям.
AccessMode :: r | w | rw | sr | sw | srw | rsw | srsw
r– в атоме означает, что поле можно читатьw– писать
Приставка s к r или w (образована от слова system) означает – только системный доступ. Исходя из вышесказанного получается, что:
r– может читать и пользователь и systemsr– может читать только systemw– может писать и пользователь и systemsw– может писать только system
Если в атоме отсутствует r – то вовсе не будет сгенерирован getter, если w – setter.
По умолчанию mode задан как rw.
Применимо к рассматриваемой модели:
1 2 3 4 5 6 7 8 9 10  |  | 
Получается, внешний пользователь может писать, но не читать пароль и вовсе никак не может влиять на соль.
Сериализация
Если необходимо задать модели сразу много полей – это не всегда удобно делать через сеттеры, т.к. приходится много раз копировать объект. для этого лучше предусмотреть функции from_proplist/1 и from_proplist/2 которые принимает proplist полей.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15  |  | 
Пример:
1 2 3 4 5 6  |  | 
Для того, чтобы получить поля модели “извне”, надо реализовать еще одну функцию – from_ext_proplist (ext от external) т.к. во-первых в ключах proplist-а уже не атомы, а бинари, а во-вторых для поля могут требоваться дополнительные трансформации.
1 2 3 4 5 6 7 8 9 10 11 12 13  |  | 
Обратите внимание, что from_ext_proplist не реализует преобразование из поля <<“salt”>>, т.к. внешнему пользователю не разрешено писать в это поле.
Еще одна полезная функция to_proplist/1, но тут все проще:
1 2 3 4 5 6 7  |  | 
А теперь вспомним про mode, сколько еще предстоит писанины… Так что оставлю реализацию to_ext_proplist на ваше усмотрение.
Теперь про tq_record_transform.
Прежде чем говорить про *_ext_* функции сразу расскажу про опции поля from_ext и to_ext.
{from_ext, Fun}– Fun применяется к данным, если Fun вернет{ok, Data}, то будет вызван сеттер с полученными данными, если{error, Reason}то, к списку ошибок трансформации будет добавлена{FieldName, Reason}.{to_ext, Fun}– Fun применяется к данным перед их отправкой пользователю. Функция должна вернуть простоData.
А также полю можно указать опцию {type, Type}. Пока она используется только для указания parse_transform-у какую from_ext функцию и какие валидаторы по умолчанию использовать, но в планах использовать ее для построения спецификации функций. Поддерживаемые на данный момент типы – binary, non_empty_binary, non_neg_integer, non_neg_float, integer, float, boolean, date, time и datetime.
Функции from_proplist и from_ext_proplist реализованы несколько по-другому. Они отличаются тем, что дополнительно принимают опции safe | unsafe и ignore_unknown, и возвращает не первую ошибку, а все ошибки, которые возникли по ходу преобразования полей.
ignore_unknown– говорит продолжать выполнение, если встретилось неизвестное поле, а не выкидывать ошибку.safe– запрещает задавать поля которые помечены как системные,unsafe– разрешает задание системных полей
to_proplist и to_ext_proplist реализованы сходным образом.
Помимо вышеперечисленных функций генерируются fields/[2,3] и ext_fields/[2,3]. На вход они принимают, помимо модели, список полей, которые надо вернуть. Т.е. работают почти как to_proplist аналоги, но возвращают только часть запрошенных полей. Опции для них:
binary_key– означает, что список полей, которые надо вернуть содержит не атомы, а binaryunsafe– вернуть и системные поля тожеignore_unknow– игнорировать неизвестные имена полей
Список измененных полей
Довольно часто встречается такая необходимость, например, при сохранении в БД или валидации. Есть два способа определить, что поле изменилось:
- Хранить старую версию и новую
 - Ставить флаг об изменении в record-е через setter
 
Первый способ заставляет хранить 2 версии одних и тех же данных, что порой слишком накладно по памяти. Так что реализуем 2-й. Для каждого record-а поля зададим еще одно с пометкой об изменении.
1 2 3 4 5 6 7 8 9 10 11  |  | 
Все сеттеры меняются по аналогичному правило, например
1 2 3 4 5 6 7 8 9 10 11 12 13 14  |  | 
Не стоит забывать про значения по умолчанию. Если они есть, то в них тоже стоит поменять маркер changed, например:
1 2 3 4 5 6 7 8 9  |  | 
Теперь 2 функции is_changed/2 и get_changed_field/2 для проверки поменялось ли поле, и получения списка измененных полей.
1 2 3 4 5  |  | 
Я использую throw вместо простого “let it fail”, мне кажется, что при таком подходе проще понять ошибку по логам.
1 2 3 4 5 6 7 8 9  |  | 
Код, сгенерированный tq_record_transform, особо ничем не отличается от приведенного выше, так что просто приведу пример использования.
1 2 3 4 5 6 7 8 9 10  |  | 
Валидаторы
Валидатор – эта функция, которая принимает на вход данные и возвращает ok или {error, Reason}.
Определим валидаторы для нашей модели:
1 2 3 4 5 6 7 8 9 10 11 12 13 14  |  | 
Логику самой функции valid я писать не буду, просто упомяну, что проблема достаточно сложная, требуется учитывать обязательное ли это поле, получено он из БД или нет (is_new/1 обсудим в следующей части), валидаторы могут выстраиваться в цепочки, могут быть заданы не только для поля, но и для модели в целом.
В tq_transform_utils определено несколько валидаторов для типов. A tq_record_transform позволяет задать опции поля required, {validators, Fun | [Fun]} и опции модели {validators, Fun | [Fun]}. Про опции модели я пока не говорил, но они задаются через атрибут -model([Opt]).
В связи с новой информацией изменим модель так чтобы она удовлетворяла следующим критериям:
login,passwordиsalt– обязательные поляloginиemailне могут быть пустыми и должны соответствовать определенным критериямpasswordне меньше 6 символов.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38  |  | 
Валидаторы:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26  |  | 
Пример:
1 2 3 4  |  | 
Итог
Если всю логику, которая описывается в декларативном виде благодаря tq_transform каждый раз описывать вручную, то потребуется огромное количество boilerplate кода а значит, количество требуемого времени и вероятность возникновения ошибок сильно увеличатся.
В следующей части я расскажу о втором плагине для tq_transtorm – tq_db, который позволяет сохранять модели в БД, о моей маленькой надстройке для sql, и том, где и как хранится мета информация о полях и модели.
P.S. Не так давно начал заниматься причесыванием кодовой базы для этих проектов, по ним пока мало документации ( на самом деле эта статья на данный момент наиболее полный справочник ). Так что, если будут вопросы, не стесняйтесь – задавайте, если пожелания или информация об ошибках и недочетах то слать их лучше или на github, или на мой email.
P.P.S Если интересно посмотреть сгенерированный код, то для этого есть специальная функция
1
 |  |