javascript

Scedel: DSL для описания схем данных

  • пятница, 20 февраля 2026 г. в 00:00:05
https://habr.com/ru/articles/1001184/

Лирическое вступление

Идея создания нового языка пришла мне в голову, когда я получил задачу описать ТЗ для подрядчика на разработку API. Передо мной встал вопрос: как легко и понятно описать требования к контрактам? Первым делом я подумал о JSON Schema, однако из-за её многословности я решил отказаться от неё, на мой взгляд, она недостаточно человекочитаема. Я перебрал еще варианты, которые могли помочь мне решить проблему: Proto, Typescript, Cue, даже об SQL подумал. Все немного не подходило под мою задачу. В итоге, я остановился на описании контракта на Typescript, и уточнении требований в комментариях.
Тогда-то мне и пришла в голову мысль разработать подходящий инструмент. Это наложилось на моё желание попрактиковаться в написании языков, и я взялся за дело.

Принципы и требования

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

Человекочитаемость и лаконичность

Описание схемы должно быть легко прочитано человеком, причём не только программистом, но человеком смежных профессий: аналитиком, PO, PM.

Language- & protocol-agnostic

Язык не должен указывать на то, какие технологии будут использоваться для реализации контракта.

Расширяемость и переиспользуемость

Необходимо давать возможность расширять правила своими собственными и дать возможность опосредованно управлять процессом валидации и генерации.

Собственный синтаксис

Хотя я взял за основу Typescript, как язык с очень богатым функционалом описания типов, решил отказаться от совместимости с каким-либо языком, так как это могло повредить читаемости и лаконичности.

Структура описания схемы

Валидатор

Валидатор - это правило проверки соответствия данных схеме. Как пример, вот описание валидатора для типа String под названием alphaOnly

validator String(alphaOnly) = this matches /[a-zA-z]+/

А вот более полное описание для валидатора, запрещающего слова, в которых больше i прописных букв подряд:

validator String(noCaps, i=2) = {  
   rule: not this matches /.*[A-Z]{$i,}.*/  
   message: 'Must not have CAPS words with length more than $i'  
}

Тип

Тип - описание структуры данных. Типы могут иметь ограничения, основанные на валидаторах.

type Status = 'banned'|'active'

type User = {  
   username: String(alphaOnly, min:3, max:10)  
   status: Status  
}

Подключение других файлов

include "./myDateTime.scedel"

type Status = 'banned'|'active'

type User = {  
   username: String(alphaOnly, min:3, max:10)  
   status: Status  
   bannedAt: when status = 'banned' then DateTimeFormatted else absent  
}

Здесь также можно увидеть механизм условных типов и особый absent, который заявляет, что поле должно отсутствовать.

Аннотации

Аннотации позволяют давать инструкции сторонним инструментам. Например, мы можем указать, в какую директорию должен складывать сгенерированные файлы генератор кода. Язык не определяет набор аннотаций, они диктуются сторонним инструментарием.

@php.codegen.namespace = "App\\Entities"  
@php.codegen.dir = "src/Entities"  
type Comment = {  
   @js.native.ignore  
   id: Uint  
    
   text: String  
    
   @php.symfony.ignore  
   createdAt: DateTimeFormatted? default now()  
}

Практически весь доступный функционал языка можно увидеть в следующем примере:

scedel-version 1.0

include "https://example.com/remote.scedel"  
include "./local.scedel"

/*  
* multiline
* comment  
*/ 
validator String(numeric) = this matches /[0-9]+/  
validator String(notNumeric) = not this matches /[0-9]+/ 
validator String(noCaps, i=2) = {  
   rule: not this matches /.*[A-Z]{$i,}.*/  
   message: 'Must not have CAPS words with length more than $i'  
}  
// line comment
type AliasType = Int  
type ConstrainedType = Float(min:10)  
type UnionType = 'First'|"Second"  
type NullableType1 = Bool|Null  
type NullableType2 = ?Bool  
type NegativeConstrainted = Url(domain: not ["localhost", '127.0.0.1'])
type RecordType = {
   field1: String(max:10)  
   fieldUnion: "Draft"|'Published'  
   conditionalField: when this.recordField.subField2 = 'https://test.com' then UnionType else absent  
   optionalField?: DateTime(format:"YYYY-MM-DD")  
   nullableField: ?IpV6  
   dependentTypeFieldFrom: DateTime(max: this.dependentTypeFieldTo)  
   dependentTypeFieldTo: DateTime(min: this.dependentTypeFieldFrom)  
   fieldWithCustomValidator: String(noCaps)  
   dictField: dict<String(max:10), String(max:255)>  
   recordField: { subField1: Binary, subField2: Url(scheme:'https') }  
}

type ArrayType = Ushort[min:1, max:3]
type ComplexType = Email(domain:'gmail.com')[min:1]|False

type IntersectType = RecordType & {additionalField: Url}

@symfony.codegen.dir='src/Entities'  
@reactjs.codegen.dir='Entities'  
type WithAnnotationsType = Uuid

@php.codegen.namespace='\\App\\Entities' on WithAnnotationsType

Конкретный пример

Давайте посмотрим, как Scedel может использоваться в реальности.
Предположим, Github выложил описание контрактов своего апи на https://api.github.com/contracts/issues.scedel

type GithubUser = {  
   id: Ulong  
   login: String(min:1)  
}

type GithubIssue = {  
   id: Ulong  
   number: Int(min:1)  
   title: String(min:1)  
   state: "open" | "closed"  
   locked: Bool  
   user: GithubUser

   closed_at:  
       when this.state = "closed"  
       then DateTime  
       else absent  
}

Вы хотите создать интеграцию своего сервиса с Гитхабом. Для этого вы можете создать собственный .scedel-файл типа:

include "https://api.github.com/contracts/issues.scedel"

@php.codegen.symfony.namespace='\\App\\Entities' on GithubUser  
@php.codegen.symfony.namespace\='\\App\\Entities' on GithubIssue

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

Что уже есть

Весь существующий материал сейчас находится на Гитхабе https://github.com/ScedelLang

Имеется довольно объемный RFC
https://github.com/ScedelLang/grammar/blob/main/RFC-Scedel-0.14.2.md

ANTLR-грамматика
https://github.com/ScedelLang/grammar/blob/main/Scedel-0.14.2.g4

Плагин для Idea для подсветки синтаксиса
https://github.com/ScedelLang/idea-plugin

И еще несколько пакетов для PHP и JS (на подходе C# и Python)
https://github.com/orgs/ScedelLang/repositories
Для каждого языка есть парсер, построитель репозитория схемы, валидатор JSON и генератор кода.

Всё распространяется под лицензией MIT.

Заключение

Конечно, продукт пока еще сырой (на что намекает текущая мажорная версия RFC 0.14.2), и может быть не готов к использованию в серьёзном продакшене. Грамматика может еще активно меняться. Есть несколько идей по развитию языка, например, глобальные аннотации и неймспейсы. Однако Scedel уже может быть вам полезен.

Буду благодарен за конструктивную критику и приглашаю поучаствовать в развитии проекта.