Русский
Русский
English
Статистика
Реклама

Codegeneration

Опыт написания IDL для embedded

24.02.2021 08:04:01 | Автор: admin

Предисловие

Я при работе с микроконтроллерами часто сталкивался с бинарными протоколами. Особенно, когда есть несколько контроллеров. Или же используется bluetooth low energy и необходимо написать код для обработки бинарных данных в характеристике. Помимо кода всегда требуется понятная документация.

Всегда возникает вопрос - а можно ли описать как-то протокол и сгенерировать на все платформы код и документацию? В этом может помочь IDL.

1. Что такое IDL

Определение IDL довольно простое и уже представлено на wikipedia

IDL, илиязык описания интерфейсов(англ.Interface Description LanguageилиInterface Definition Language)язык спецификацийдля описанияинтерфейсов, синтаксически похожий на описание классов в языкеC++.

Самое главное в IDL - он должен хорошо описывать интерфейс взаимодействия, API, протокол. Он должен быть достаточно понятен, чтобы служить другим инженерам документацией.

Бонус также является - генерация документации, структур, кода.

2. Мотивация

В процессе работы я попробовал разные кодогенераторы и IDL. Среди тех, что попробовал были - QFace (http://personeltest.ru/aways/github.com/Pelagicore/qface), swagger (Это не IDL, а API development tool). Также существует коммерческое решение проблемы: https://www.protlr.com/.

Swagger больше подходит к REST API. Поэтому сразу был отметён. Однако его можно использовать если применяется cbor (бинарный аналог json с кучей крутых фич).

В QFace давно не было коммитов, хотелось некоторых "наворотов" для применения в embedded, возникли сложности при написании шаблона. Он не ищет символы сам, не умеет считать поля enum-ов.

Бесплатные решения было найти сложно, чтобы можно было комфортно использовать при разработке бинарных протоколов.

Поэтому я отказался от генераторов кода и IDL в пользу написания некоторых "автоматизаций" в коде, позволяющих проще писать адаптер протокола. Но протокол с коллегами продолжили описывать при помощи QFace. Решил в свободное время попробовать сделать что-то более или менее годное.

2.1 Обзор QFace

IDL, которая являлась источником вдохновения, имеет простой синтаксис:

module <module> <version>import <module> <version>interface <Identifier> {    <type> <identifier>    <type> <operation>(<parameter>*)    signal <signal>(<parameter>*)}struct <Identifier> {    <type> <identifier>;}enum <Identifier> {    <name> = <value>,}flag <Identifier> {    <name> = <value>,}

Для генерации используется jinja2. Пример:

{% for module in system.modules %}    {%- for interface in module.interfaces -%}    INTERFACE, {{module}}.{{interface}}    {% endfor -%}    {%- for struct in module.structs -%}    STRUCT , {{module}}.{{struct}}    {% endfor -%}    {%- for enum in module.enums -%}    ENUM   , {{module}}.{{enum}}    {% endfor -%}{% endfor %}

Концепция интересная. Можно было просто "подпилить" для комфорта "напильником", что конечно и сделал мой коллега. Но мне показалось интересным взять библиотеку sly и просто написать IDL с нужными фичами.

3. Обзор sly

Почему именно sly - библиотека очень проста для описания грамматики.

Сначала надо написать лексер. Он токенизирует код чтобы далее было проще обрабатывать парсером. Код из документации:

class CalcLexer(Lexer):    # Set of token names.   This is always required    tokens = { ID, NUMBER, PLUS, MINUS, TIMES,               DIVIDE, ASSIGN, LPAREN, RPAREN }    # String containing ignored characters between tokens    ignore = ' \t'    # Regular expression rules for tokens    ID      = r'[a-zA-Z_][a-zA-Z0-9_]*'    NUMBER  = r'\d+'    PLUS    = r'\+'    MINUS   = r'-'    TIMES   = r'\*'    DIVIDE  = r'/'    ASSIGN  = r'='    LPAREN  = r'\('    RPAREN  = r'\)'

Нужно наследовать класс Lexer, в переменную tokens - добавить свои использованные токены. Само определение токенов делается в теле класса - достаточно просто описать регулярное выражение, соответсвующее токену.

Парсер - делает работу по преобразованию набора токенов по определенным правилам. С помощью его и осуществляется основная работа. В случае компиляторов - преобразование в байт-код/объектный файл итд. Для интерпретаторов - можно сразу выполнять вычисления. При реализации кодогенератора - можно преобразовать в дерево классов.

Также парсер задается очень простым способом (пример из документации):

class CalcParser(Parser):    # Get the token list from the lexer (required)    tokens = CalcLexer.tokens    # Grammar rules and actions    @_('expr PLUS term')    def expr(self, p):        return p.expr + p.term    @_('expr MINUS term')    def expr(self, p):        return p.expr - p.term    @_('term')    def expr(self, p):        return p.term    @_('term TIMES factor')    def term(self, p):        return p.term * p.factor    @_('term DIVIDE factor')    def term(self, p):        return p.term / p.factor    @_('factor')    def term(self, p):        return p.factor    @_('NUMBER')    def factor(self, p):        return p.NUMBER    @_('LPAREN expr RPAREN')    def factor(self, p):        return p.expr

Каждый метод класса отвечает за парсинг конкретной конструкции. В декораторе @_ указывается правило, которое обрабатывается. Имя метода sly распознает как название правила.

В этом примере сразу происходят вычисления.

Подробнее можно прочитать в официальной документации: https://sly.readthedocs.io/en/latest/sly.html

4. Процесс создания

В самом начале программа получает yml файл с настройками. Затем при помощи sly преобразовывает код в древо классов. Далее выполняются вычисления и поиски объектов. После вычисления - передается в jinja2 шаблон и дерево символов.

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

Вначале определили, что модуль состоит из списка термов:

    @_('term term')    def term(self, p):        t0 = p.term0        t1 = p.term1        t0.extend(t1)        return t0

Затем определим, что терм состоит из определений структуры, энумератора или интерфейса разделенные символом ";"(SEPARATOR):

   @_('enum_def SEPARATOR')    def term(self, p):        return [p.enum_def]    @_('statement SEPARATOR')    def term(self, p):        return [p.statement]    @_('interface SEPARATOR')    def term(self, p):        return [p.interface]    @_('struct SEPARATOR')    def term(self, p):        return [p.struct]

Здесь терм сразу паковался в массив для удобства. Чтобы список термов (term term правило) работал уже сразу с листами и собрал в один лист.

Ниже представлен набор правил для описания структуры:

    @_('STRUCT NAME LBRACE struct_items RBRACE')    def struct(self, p):        return Struct(p.NAME, p.struct_items, lineno=p.lineno)    @_('decorator_item STRUCT NAME LBRACE struct_items RBRACE')    def struct(self, p):        return Struct(p.NAME, p.struct_items, lineno=p.lineno, tags=p.decorator_item)    @_('struct_items struct_items')    def struct_items(self, p):        si0 = p.struct_items0        si0.extend(p.struct_items1)        return si0    @_('type_def NAME SEPARATOR')    def struct_items(self, p):        return [StructField(p.type_def, p.NAME, lineno=p.lineno)]    @_('type_def NAME COLON NUMBER SEPARATOR')    def struct_items(self, p):        return [StructField(p.type_def, p.NAME, bitsize=p.NUMBER, lineno=p.lineno)]    @_('decorator_item type_def NAME SEPARATOR')    def struct_items(self, p):        return [StructField(p.type_def, p.NAME, lineno=p.lineno, tags=p.decorator_item)]    @_('decorator_item type_def NAME COLON NUMBER SEPARATOR')    def struct_items(self, p):        return [StructField(p.type_def, p.NAME, bitsize=p.NUMBER, lineno=p.lineno, tags=p.decorator_item)]

Если описать простым языком правила - структура (struct) содержит поля структур (struct_items). А поля структур могут определяться как:

  • тип (type_def), имя (NAME), разделитель (SEPARATOR)

  • тип (type_def), имя, двоеточие (COLON), число (NUMBER - для битфилда, означает количество бит), разделитель

  • список декораторов (decorator_item), тип, имя, разделитель

  • список декораторов, тип, имя, двоеточие (COLON), число (NUMBER - для битфилда), разделитель

Новшество относительно QFace (однако есть в protlr) - была введена возможность описывать специальные условные ссылки на структуры. Было решено назвать эту фичу - alias.

    @_('DECORATOR ALIAS NAME COLON expr struct SEPARATOR')    def term(self, p):        return [Alias(p.NAME, p.expr, p.struct), p.struct]

Это было сделано чтобы поддерживалась следующая конструкция:

enum Opcode {    Start =  0x00,    Stop = 0x01};@alias Payload: Opcode.Startstruct StartPayload {...};@alias Payload: Opcode.Stopstruct StopPayload {...};struct Message {    Opcode opcode: 8;    Payload<opcode> payload;};

Данная конструкция обозначает, что если opcode = Opcode.Start (0x00) - payload будет соответствовать структуре StartPayload. Если opcode = Opcode.Stop (0x01) - payload будет иметь структуру StopPayload. То есть создаем ссылку структуры с определенными условиями.

Следующее что было сделано - отказался от объявления модуля. Показалось это избыточным так как - имя файла уже содержит имя модуля, а версию писать бессмысленно так как есть git. Хороший протокол имеет прямую и обратную совместимость и в версии нуждаться не должен. Был выкинут тип flag так как есть enum, и добавил возможность описания битфилдов. Убрал возможность определения сигналов так как пока что низкоуровневого примера, демонстрирующего пользу, не было.

Была добавлена возможность python-подобных импортов. Чтобы можно было импортировать из другого модуля только конкретный символ. Это полезно для генерации документации.

Для вычислений был создан класс - Solvable. Его наследует каждый объект, которому есть что посчитать. Например, для SymbolType (тип поля класса или интерфейса). В данном классе этот метод ищет по ссылке тип, чтобы добавить его в поле reference. Чтобы в jinja можно было сразу на месте обратиться к полям enum или структуры. Класс Solvable должен искать во вложенных символах вычислимые и вызывать solve. Т.е. вычисления происходят рекурсивно.

Пример реализации метода solve для структуры:

    def solve(self, scopes: list):        scopes = scopes + [self]        for i in self.items:            if isinstance(i, Solvable):                i.solve(scopes=scopes)

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

struct SomeStruct {i32someNumber;@setter: someNumber;void setInteger(i32 integer);};

Как видно из примера - это позволяет производить поиск символа someNumber в области видимости структуры, вместо явного указания SomeStruct.someNumber.

Заключение

По сравнению с QFace мне удалось - упростить написание шаблона за счет поиска типов, вычисления перечислений. Также полезно иметь импорт символов и возможность условно ссылаться на разные структуры.

В папке examples/uart - находится пример генерации заголовков, кода и html документации. Пример иллюстрирует типичный uart протокол с применением новых фич. Подразумевается, что функции типа put_u32 итд - определит сам пользователь исходя из порядка байт и архитектуры MCU.

Ознакомиться подробнее с реализацией можно по ссылке: https://gitlab.com/volodyaleo/volk-idl

P.S.

Это моя первая статья на Хабр. Буду рад получить отзывы - интересна ли данная тематика или нет. Если у кого-то есть хорошие примеры кодо+доко-генераторов бинарных протоколов для Embedded, было бы интересно прочитать в комментариях. Или какая-то успешная практика внедрения похожих систем для описания бинарных протоколов.

В данном проекте я не обращал особого внимания на скорость работы. Некоторые вещи делал чтобы "быстрее решить задачу". Было важнее получить рабочий код, который можно уже пробовать применять к разным проектам.

Подробнее..

Как готовить nullable reference types с добавлением appsettings.json

30.08.2020 22:21:16 | Автор: admin
В данной статье хочу поделиться своими размышлениями о том, можно ли на современном C# писать код, безопасный от NullReferenceException. Этот зловредный тип исключения не говорит разработчику, в каком конкретно месте у него null. Конечно, от отчаяния можно?.начать?.писать?.обращение?.ко?.всем?.полям?.вот?.так?.вот, но есть адекватное решение использовать аннотации типов от JetBrains или Microsoft. После этого компилятор начнет нам подсказывать (и подсказывать очень настойчиво, если включить опцию WarningsAsError), в каком конкретно месте нужно добавлять соответствующую проверку.
Но все ли так гладко? Под катом я хочу разобрать и предложить решение одной конкретной проблемы.




Постановка задачи


Примечание: Подразумевается, что весь код в этой статье будет компилироваться с параметрами проекта:

<TreatWarningsAsErrors>true</TreatWarningsAsErrors><Nullable>enable</Nullable>


Предположим, мы хотим написать класс, который принимает определенный набор параметров, необходимый ему для работы:

    public sealed class SomeClient    {        private readonly SomeClientOptions options;        public SomeClient(SomeClientOptions options)        {            this.options = options;        }        public void SendSomeRequest()        {            Console.WriteLine($"Do work with { this.options.Login.ToLower() }" +                $" and { this.options.CertificatePath.ToLower() }");        }    }


Таким образом, мы хотели бы объявить некий контракт и сообщить клиентскому коду, что он не должен передавать Login и CertificatePath со значениями null. Поэтому класс SomeClientOptions можно было бы написать как-то так:

    public sealed class SomeClientOptions    {        public string Login { get; set; }        public string CertificatePath { get; set; }        public SomeClientOptions(string login, string certificatePath)        {            Login = login;            CertificatePath = certificatePath;        }    }


Второе вполне очевидное требование к приложению в целом (особенно это актуально для asp.net core): иметь возможность получать наш SomeClientOptions из какого-нибудь json файла, который можно удобно модифицировать во время деплоя.
Поэтому дописываем одноименную секцию в appsettings.json:

{  "SomeClientOptions": {    "Login": "ferzisdis",    "CertificatePath":  ".\full_access.pfx"  }}


Ну а теперь вопрос: как нам создать объект SomeClientOptions и гарантировать, что все NotNull поля не будут возвращать null не при каких обстоятельствах?

Наивная попытка использовать встроенные инструменты


Мне хотелось бы написать примерно такой блок кода, а не строчить статью на Хабр:

    public class Startup    {        public Startup(IConfiguration configuration)        {            Configuration = configuration;        }        public IConfiguration Configuration { get; }        public void ConfigureServices(IServiceCollection services)        {            var options = Configuration.GetSection(nameof(SomeClientOptions)).Get<SomeClientOptions>();            services.AddSingleton(options);        }    }


Но этот код неработоспособен, т.к. метод Get() накладывает ряд ограничений на тип, с которым работает:
Тип T должен быть неабстрактным и содержать открытый конструктор без параметров
Гетеры свойств не должны генерировать исключений

Учитывая указанные ограничения, мы вынуждены переделать класс SomeClientOptions примерно таким образом:

public sealed class SomeClientOptions    {        private string login = null!;        private string certificatePath = null!;        public string Login        {            get            {                return login;            }            set            {                login = !string.IsNullOrEmpty(value) ? value : throw new InvalidOperationException($"{nameof(Login)} cannot be null!");            }        }        public string CertificatePath        {            get            {                return certificatePath;            }            set            {                certificatePath = !string.IsNullOrEmpty(value) ? value : throw new InvalidOperationException($"{nameof(CertificatePath)} cannot be null!");            }        }    }


Думаю, вы со мной согласитесь, такое решение не является ни красивым, ни правильным. Как минимум потому, что клиенту ничего не мешает просто создать этот тип через конструктор и передать его объекту SomeClient на этапе компиляции не будет выдано ни единого предупреждения, а в рантайме получим заветный NRE.

Примечание: В качестве проверки на null я буду использовать string.IsNullOrEmpty(), т.к. в большенстве случаев пустую строку можно интерпретировать как незаданное значение

Альтернативы получше


Предлагаю сначала разобрать несколько правильных способов решить задачу, которые имеют очевидные недостатки.

Можно разбить SomeClientOptions на два объекта, где первый используется для десериализации, а второй производит валидацию:

    public sealed class SomeClientOptionsRaw    {        public string? Login { get; set; }        public string? CertificatePath { get; set; }    }    public sealed class SomeClientOptions : ISomeClientOptions    {        private readonly SomeClientOptionsRaw raw;        public SomeClientOptions(SomeClientOptionsRaw raw)        {            this.raw = raw;        }        public string Login            => !string.IsNullOrEmpty(this.raw.Login) ? this.raw.Login : throw new InvalidOperationException($"{nameof(Login)} cannot be null!");        public string CertificatePath            => !string.IsNullOrEmpty(this.raw.CertificatePath) ? this.raw.CertificatePath : throw new InvalidOperationException($"{nameof(CertificatePath)} cannot be null!");    }    public interface ISomeClientOptions    {        public string Login { get; }        public string CertificatePath { get; }    }


Считаю это решение достаточно простым и изящным, за исключением того, что программисту каждый раз придется создавать на один класс больше и дублировать набор свойств.
Гораздо правильнее изначально было бы использовать в SomeClient вместо SomeClientOptions интерфейс ISomeClientOptions (как мы убедились, реализация может очень сильно зависеть от окружения).

Второй (менее элегантный) способ вытаскивать вручную значения из IConfiguration:

    public sealed class SomeClientOptions : ISomeClientOptions    {        private readonly IConfiguration configuration;        public SomeClientOptions(IConfiguration configuration)        {            this.configuration = configuration;        }        public string Login => GetNotNullValue(nameof(Login));        public string CertificatePath => GetNotNullValue(nameof(CertificatePath));        private string GetNotNullValue(string propertyName)        {            var value = configuration[$"{nameof(SomeClientOptions)}:{propertyName}"];            return !string.IsNullOrEmpty(value) ? value : throw new InvalidOperationException($"{propertyName} cannot be null!");        }    }


Такой подход мне не нравится из-за необходимости самостоятельной реализации процесса парсинга и конвертации типов.
К тому же, вы не считаете, что слишком уж много сложностей для такой маленькой задачи?

Как не писать руками лишний код?


Основная идея заключается в том, чтобы для интерфейса ISomeClientOptions генерировать реализацию в runtime, включающую все необходимые проверки. В статье я хочу предложить лишь концепт решения. Если тема достаточно заинтересует сообщество, подготовлю nuget-пакет для боевого применения (с открытыми исходниками на гитхабе).

Для простоты реализации, я разбил всю процедуру на 3 логические части:

  1. Создается runtime реализация интерфейса
  2. Выполняется десериализация объекта стандартными средствами
  3. Выполняется проверка свойств на null (проверяются только те свойста, которые отмечены как NotNull)


    public static class ConfigurationExtensions    {        private static readonly InterfaceImplementationBuilder InterfaceImplementationBuilder = new InterfaceImplementationBuilder();        private static readonly NullReferenceValidator NullReferenceValidator = new NullReferenceValidator();        public static T GetOptions<T>(this IConfiguration configuration, string sectionName)        {            var implementationOfInterface = InterfaceImplementationBuilder.BuildClass<T>();            var options = configuration.GetSection(sectionName).Get(implementationOfInterface);            NullReferenceValidator.CheckNotNullProperties<T>(options);            return (T) options;        }    }

InterfaceImplementationBuilder
    public sealed class InterfaceImplementationBuilder    {        private readonly Lazy<ModuleBuilder> _module;        public InterfaceImplementationBuilder()        {            _module = new Lazy<ModuleBuilder>(() => AssemblyBuilder                .DefineDynamicAssembly(new AssemblyName(Guid.NewGuid().ToString()), AssemblyBuilderAccess.Run)                .DefineDynamicModule("MainModule"));        }        public Type BuildClass<TInterface>()        {            return BuildClass(typeof(TInterface));        }        public Type BuildClass(Type implementingInterface)        {            if (!implementingInterface.IsInterface)            {                throw new InvalidOperationException("Only interface is supported");            }            var typeBuilder = DefineNewType(implementingInterface.Name);            ImplementInterface(typeBuilder, implementingInterface);            return typeBuilder.CreateType() ?? throw new InvalidOperationException("Cannot build type!");        }        private void ImplementInterface(TypeBuilder typeBuilder, Type implementingInterface)        {            foreach (var propertyInfo in implementingInterface.GetProperties())            {                DefineNewProperty(typeBuilder, propertyInfo.Name, propertyInfo.PropertyType);            }                        typeBuilder.AddInterfaceImplementation(implementingInterface);        }           private TypeBuilder DefineNewType(string baseName)        {            return _module.Value.DefineType($"{baseName}_{Guid.NewGuid():N}");        }        private static void DefineNewProperty(TypeBuilder typeBuilder, string propertyName, Type propertyType)        {            FieldBuilder fieldBuilder = typeBuilder.DefineField("_" + propertyName, propertyType, FieldAttributes.Private);            PropertyBuilder propertyBuilder = typeBuilder.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null);            MethodBuilder getPropMthdBldr = typeBuilder.DefineMethod("get_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig | MethodAttributes.Virtual, propertyType, Type.EmptyTypes);            ILGenerator getIl = getPropMthdBldr.GetILGenerator();            getIl.Emit(OpCodes.Ldarg_0);            getIl.Emit(OpCodes.Ldfld, fieldBuilder);            getIl.Emit(OpCodes.Ret);            MethodBuilder setPropMthdBldr =                typeBuilder.DefineMethod("set_" + propertyName,                    MethodAttributes.Public                    | MethodAttributes.SpecialName                    | MethodAttributes.HideBySig                    | MethodAttributes.Virtual,                    null, new[] { propertyType });            ILGenerator setIl = setPropMthdBldr.GetILGenerator();            Label modifyProperty = setIl.DefineLabel();            Label exitSet = setIl.DefineLabel();            setIl.MarkLabel(modifyProperty);            setIl.Emit(OpCodes.Ldarg_0);            setIl.Emit(OpCodes.Ldarg_1);            setIl.Emit(OpCodes.Stfld, fieldBuilder);            setIl.Emit(OpCodes.Nop);            setIl.MarkLabel(exitSet);            setIl.Emit(OpCodes.Ret);            propertyBuilder.SetGetMethod(getPropMthdBldr);            propertyBuilder.SetSetMethod(setPropMthdBldr);        }    }


NullReferenceValidator
    public sealed class NullReferenceValidator    {        public void CheckNotNullProperties<TInterface>(object options)        {            var propertyInfos = typeof(TInterface).GetProperties();            foreach (var propertyInfo in propertyInfos)            {                if (propertyInfo.PropertyType.IsValueType)                {                    continue;                }                if (!IsNullable(propertyInfo) && IsNull(propertyInfo, options))                {                    throw new InvalidOperationException($"Property {propertyInfo.Name} cannot be null!");                }            }        }        private bool IsNull(PropertyInfo propertyInfo, object obj)        {            var value = propertyInfo.GetValue(obj);            switch (value)            {                case string s: return string.IsNullOrEmpty(s);                default: return value == null;            }        }        // https://stackoverflow.com/questions/58453972/how-to-use-net-reflection-to-check-for-nullable-reference-type        private bool IsNullable(PropertyInfo property)        {            if (property.PropertyType.IsValueType)            {                throw new ArgumentException("Property must be a reference type", nameof(property));            }            var nullable = property.CustomAttributes                .FirstOrDefault(x => x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableAttribute");            if (nullable != null && nullable.ConstructorArguments.Count == 1)            {                var attributeArgument = nullable.ConstructorArguments[0];                if (attributeArgument.ArgumentType == typeof(byte[]) && attributeArgument.Value != null)                {                    var args = (ReadOnlyCollection<CustomAttributeTypedArgument>)attributeArgument.Value;                    if (args.Count > 0 && args[0].ArgumentType == typeof(byte))                    {                        return (byte)args[0].Value == 2;                    }                }                else if (attributeArgument.ArgumentType == typeof(byte))                {                    return (byte)attributeArgument.Value == 2;                }            }            var context = property.DeclaringType.CustomAttributes                .FirstOrDefault(x => x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableContextAttribute");            if (context != null &&                context.ConstructorArguments.Count == 1 &&                context.ConstructorArguments[0].ArgumentType == typeof(byte) &&                context.ConstructorArguments[0].Value != null)            {                return (byte)context.ConstructorArguments[0].Value == 2;            }            // Couldn't find a suitable attribute            return false;        }    }


Пример использования:
    public class Startup    {        public Startup(IConfiguration configuration)        {            Configuration = configuration;        }        public IConfiguration Configuration { get; }        public void ConfigureServices(IServiceCollection services)        {            var options = Configuration.GetOptions<ISomeClientOptions>("SomeClientOptions");            services.AddSingleton(options);        }    }


Заключение


Таким образом, использование nullabe reference types не так тривиально, как может показаться на первый взгляд. Этот инструмент позволяет лишь снизить количество NRE, а не избавиться от них полностью. Да и многие библиотеки еще не аннатированы должным образом.

Спасибо за уделенное внимание. Надеюсь, вам понравилась статья.
Расскажите, сталкивались ли вы с подобной проблемой и как обходили ее. Буду благодарен за комментарии к предложенному решению.
Подробнее..

Перевод Кодогенерацию с использованием Roslyn можно использовать и без перехода на .Net 5

26.02.2021 00:21:59 | Автор: admin


Недавно, когда я просматривал новые возможности, которые будут включены в .Net 5, я натолкнулся на одну весьма интересную генераторы исходного кода. Этот функционал меня особенно заинтересовал, так как я использую аналогичный подход в течение последних 5 лет, и то, что предлагает Microsoft это просто более глубокая интеграция этого подхода в процесс сборки проектов.


Примечание: Оригинал был написан в момент, когда релиз .Net 5 только-только собирался выйти, но актуальности этот текст, на мой взгляд, не потерял, поскольку переход на новую версию платформы занимает какое-то время, да и принципы работы с Roslyn никак не поменялись.


Далее я поделюсь своим опытом использования Roslyn при генерации кода, и надеюсь, что это поможет вам лучше понять, что именно предлагает Microsoft в .Net 5 и в каких случаях это можно использовать.


Для начала, давайте рассмотрим типичный сценарий генерации исходного кода. У вас есть некий внешний источник информации например такой как база данных или JSON описание какого-нибудь REST сервиса или другая .Net сборка (через рефлексию) или что-нибудь еще, и с помощью этой информации вы можете сгенерировать различные типы исходного кода, такие как DTO, классы моделей базы данных или прокси для REST сервисов.


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


По совпадению, я недавно опубликовал проект с открытым исходным кодом, в котором есть пример такой ситуации. В проекте более 100 классов, которые представляют узлы синтаксического дерева SQL, и мне нужно было создать посетителей (visitors реализации интерфейса IVisitor в советующем шаблоне проектирования), которые будут обходить и изменять объекты дерева (больше информации о проекте вы можете найти в моей предыдущей статье "Дерево синтаксиса и альтернатива LINQ при взаимодействии с базами данных SQL").


Причина, по которой генерация кода является здесь хорошим выбором, заключается в том, что каждый раз, когда я делаю даже небольшое изменение в классах, мне нужно помнить об изменении посетителей (visitors), и эти изменения должны выполняться очень осторожно. Однако я не могу использовать рефлексию для генерации кода, так как сборка (assembly), которая содержит эти новые изменения, просто еще не существует, и если эти изменения несовместимы с предыдущей версией и приводят к ошибкам компиляции, то эта сборка никогда и не появится до тех пор, пока я вручную не исправлю все ошибки.


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


Давайте создадим простое консольное приложение и добавим в него пакет Microsoft.CodeAnalysis.CSharp.


Примечание: теоретически это можно сделать и через t4 (без консольного приложения), но я предпочитаю не бороться с добавлением в него ссылок на dll и странным синтаксисом, при отсутствии нормального редактора.


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


var files = Directory.EnumerateFiles(    Path.Combine(projectFolder, "Syntax"),     "*.cs",     SearchOption.AllDirectories);files = files.Concat(Directory.EnumerateFiles(projectFolder, "IExpr*.cs"));var trees = files    .Select(f => CSharpSyntaxTree.ParseText(File.ReadAllText(f)))    .ToList();

Синтаксические деревья содержат много информации об исходном коде с точки зрения текста (имена классов, имена методов и т. д.), но часто этой информации недостаточно, поскольку мы хотим знать, что же этот текст означает, и поэтому нам нужно попросить Roslyn проанализировать синтаксические деревья для того, чтобы получить семантические данные:


var cSharpCompilation = CSharpCompilation.Create("Syntax", trees);foreach (var tree in trees){    var semantic = cSharpCompilation.GetSemanticModel(tree);    ...

Используя семантические данные, мы можем получить объект типа INamedTypeSymbol:


foreach (var classDeclarationSyntax in tree    .GetRoot()    .DescendantNodesAndSelf()    .OfType<ClassDeclarationSyntax>()){    var classSymbol = semantic.GetDeclaredSymbol(classDeclarationSyntax);

который может предоставить информацию о конструкторах и свойствах классов:


//Propertiesvar properties = GetProperties(classSymbol);List<ISymbol> GetProperties(INamedTypeSymbol symbol){    List<ISymbol> result = new List<ISymbol>();    while (symbol != null)    {        result.AddRange(symbol.GetMembers()            .Where(m => m.Kind == SymbolKind.Property));        symbol = symbol.BaseType;    }    return result;}//Constructorsforeach (var constructor in classSymbol.Constructors){    ...}

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


foreach (var parameter in constructor.Parameters){    ...    INamedTypeSymbol pType = (INamedTypeSymbol)parameter.Type;

Теперь необходимо проанализировать каждый тип параметра и выяснить следующее:


  1. Является ли этот тип списком?
  2. Является ли тип Nullable (в проекте используются "Nullable reference types")?
  3. Наследуется ли от этот тип от базового типа (в нашем случае интерфейса), для которого мы и создаем "Посетителей" (Visitors).

Семантическая модель дает ответы на эти вопросы:


var ta = AnalyzeSymbol(ref pType);....(bool IsNullable, bool IsList, bool Expr) AnalyzeSymbol(    ref INamedTypeSymbol typeSymbol){    bool isList = false;    var nullable = typeSymbol.NullableAnnotation == NullableAnnotation.Annotated;    if (nullable && typeSymbol.Name == "Nullable")    {        typeSymbol = (INamedTypeSymbol)typeSymbol.TypeArguments.Single();    }    if (typeSymbol.IsGenericType)    {        if (typeSymbol.Name.Contains("List"))        {            isList = true;        }        if (typeSymbol.Name == "Nullable")        {            nullable = true;        }        typeSymbol = (INamedTypeSymbol)typeSymbol.TypeArguments.Single();    }    return (nullable, isList, IsExpr(typeSymbol));}

Примечание: метод AnalyzeSymbol извлекает фактический тип из коллекций и значений Nullables::


List<T> => T (list := true) T? => T (nullable := true) List<T>? => T (list := true, nullable := true)

Проверка базового типа в семантической модели Roslyn является более сложной задачей, чем такая же при использовании рефлексии, но это также возможно:


bool IsExpr(INamedTypeSymbol symbol){    while (symbol != null)    {        if (symbol.Interfaces.Any(NameIsExpr))        {            return true;        }        symbol = symbol.BaseType;    }    return false;    bool NameIsExpr(INamedTypeSymbol iSym)    {        if (iSym.Name == "IExpr")        {            return true;        }        return IsExpr(iSym);    }}

Теперь мы можем поместить всю эту информацию в простой контейнер:


public class NodeModel{    ...    public string TypeName { get; }    public bool IsSingleton { get; }    public IReadOnlyList<SubNodeModel> SubNodes { get; }    public IReadOnlyList<SubNodeModel> Properties { get; }}public class SubNodeModel{    ...    public string PropertyName { get; }    public string ConstructorArgumentName { get; }    public string PropertyType { get; }    public bool IsList { get; }    public bool IsNullable { get; }}

и использовать его при генерации кода, получая при этом что-то вроде этого (большой класс с кучей однотипных методов). Ссылка на сам генератор в конце статьи.


В моем проекте я запускаю генерацию кода как консольную утилиту, но в .Net 5 вы сможете встроить эту генерацию в класс реализующий интерфейс ISourceGenerator и помеченный специальным атрибутом Generator. Экземпляр этого класса будет автоматически создаваться и запускаться во время сборки проекта для добавления недостающих частей кода. Это, конечно, удобнее, чем отдельная утилита, но идея аналогична.


Примечание: Я здесь не буду описывать саму кодогенерацию в .Net 5 так как в интернете есть много информации об этом, например ссылка 1 или ссылка 2


В завершение, я хочу сказать, что вы не должны воспринимать эту новую возможность .Net 5 как невероятное нововведение, которое коренным образом изменит подход к генерации динамического кода, используемый в таких библиотеках, как AutoMapper, Dapper и т. д. (слышал и такие мнения) Не изменит! Дело в том, что описанная выше генерация кода работает в статическом контексте, где все заранее известно, но, например, AutoMapper не знает заранее, с какими классами он будет работать, и ему все равно придется динамически генерировать IL код "на лету". Однако бывают ситуации, когда такая генерация кода может быть весьма полезна (одну из них ситуаций я описал в этой статье). Поэтому стоит, как минимум, знать об этой возможности и понимать ее принципы и ограничения.


Ссылка на исходный код на github

Подробнее..
Категории: C , Net , Codegeneration , Net 5

Из песочницы Как я писал кодогенератор на PHP и что из этого получилось

18.07.2020 18:23:19 | Автор: admin

Причины и проблемы, которые нужно было решить


В этой статье я вам расскажу о том как я писал кодогенератор на php. Расскажу о пути, который он прошел от генерации простых таблиц, до довольно полноценного генератора html и css кода. Приведу примеры его использования и покажу уже сделанные проекты.


В этом семестре на одном из предметов можно было использовать только PHP.


После бесконеного ренейма проекта Проект получил имя MelonPHP. Чтобы люди думали о еде когда произносили его имя? Но у нас тут статья не о генерации бреда, по этому давайте я вам расскажу о причине его создания.


Написать надо было много, но это не проблема. Основная проблема заключалась в выводе HTML кода через PHP. Я постараюсь объяснить проблему ниже.


Например вот вывод текста через всем знакомое echo:


$text = "out text";echo "<p>$text</p>";

Выглядит просто и понятно. Давайте возьмем отрезок кода моего друга, где он генерирует таблицу:


...$sql = "SELECT * FROM table";$result = $conn->query($sql);if($result->num_rows > 0) {    echo "<b>Table table</b><br><br>";    echo "<table border=2>";  echo "<tr><td> name </td>"."<td> name </td>"."<td> name </td></tr>";    while($row = $result->fetch_assoc()) {        echo "<tr><td>".$row["name"]."</td><td>".$row["name"]."</td><td>".$row["name"]."</td></tr>";    }    echo "</table>";} else {echo "0 results";}...

Это страшный код демонстрирует проблемы, которые я хотел решить:


  • Присутствие html в php коде, что делает его по моему мнению менее читаемым. Все-таки файл для одного яп должен содержать код только одного яп(а), по моему мнению
  • Нет разделения логики, все в каше. Хотелось более приятный "фронтенд" на PHP

Стоит отметить, что я относительно давно пишу на Flutter и мне очень нравится идея заложенная в его основе, связанная с написанием интерфейса с помощью постройки дерева из виджетов. Я решил позаимствовать оттуда идею с нодами (виджетами).


Я был уверен, что быстрее будет написать небольшой кодогенератор, чем терпеть это.


Изначально генератор занимался генерацией таблиц через функции. Но потом перерос в ней-то более масштабное.


Основными идеями были следующими:


  • UI пишется из элементов/компонентов (привет React)
  • Удобные макеты (Избавиться от div, div, div, div...)
  • Чтобы весь UI писался на PHP (без JS, без HTML, без CSS).
  • Rebuild через callback события, через AJAX + JQuery не суждено
  • Удобная система роутов не суждено
  • Поддержка CSS (и не просто строку писать, на уровне "width: 100px", а полноценная поддержка прямо в PHP коде)
  • ООП

Особенности MelonPHP


  • Почти все элементы (кроме текста, кнопки и еще нескольких) по умолчанию имеют ширину и высоту в 100%, в том числе и документ.
  • Кроме того, если элементы будут выходить за пределы страницы, то скролла по умолчанию не будет. Для этого нужно использовать ScrollView.
  • Так же по умолчанию нельзя выделать никакие элементы.

Архитектура


Очень печально, что в PHP отсутствует передачу параметров по имени, и по этому я решил использовать вместо них функции. Я уверен что это не менее удобный аналог (Какого было мое удивление, когда Microsoft показали MAUI, который использует ту же идею с функциями).


Все классы в MelonPHP наследуется от Node. Это простой класс, который имеет только 2 функции: Generate(), static Create().


  • Generate() возвращает string сгенерированный код.
  • Create() это статическая функция. Она нужна чтобы было проще создавать ноды в дереве.

abstract class Node{  abstract function Generate() : string;  static function Create() ...}

Element

Element это более высокоуровневый класс, который нужен для более комфортного написания своих элементов.


Элемент в основном занимается генерацией чистого html кода.


Элементами в Фреймворк являются такие сущности как контейнер, кнопка, таблица и тд.


Component

Основная идея компонента в том, что этот класс управляет, и состоит из дерева элементов в нем. Компонент наследуется от элемента (бредовая идея).


Компонентами могут быть например дисплеи (аналог страниц в MelonPHP), списки, карточки, навигационные меню и тд.


abstract class Component extends Element{  function Initialize() ...  abstract function Build() : Element;  function Detach() ...}

Компонент удобен тем что вам не надо генерировать строки, чем занимается Element, так же в нем есть удобные функции для использования логики.


Попробуем написать простой компонент. Создадим класс ListItem наследуемый от Component.


Перезапишем функции Initialize() и Build().


Initialize() вызывается при создание компонента. В ней например можно инициализировать переменные или обработать логику.

Build() вызывается при генерации элемента. В ней обязательно должен возвращаться элемент. Обязательная для перезаписи.

Detach() вызывается при удалении компонента.

В Build() возвратим контейнер, а в качестве его ребенка, элемент текста и присвоим ему текст из переменной класса $Text.


В Initialize() пропишем значение $Text по умолчанию.


Добавим функцию Text(string) в которой будет записываться значение пользователя в переменную $Text.


Обязательно надо возвращать $this в функциях, которые будут вызываться в дереве.

class ListItem extends Component{  private $Text;  function Initialize() {    $this->Text = "Name";  }  function Build() : Element {    return Container::Create()    ->Child(      Text::Create()      ->Text($this->Text)    );  }  function Text(string $string) {    $this->Text = $string;    return $this;  }}

DisplayComponent

DisplayComponent это компонент, который может выводит сгенерированный код на страницу. Для генерации нужно вызвать функцию Display.


Попробуем написать пример простого дисплея.


В функции Build() возвратим Document и присваиваем ему Title(string).


В DisplayComponent, в функции Build() всегда должен возвращаться Document. Document это класс, который генерирует стандартную разметку HTML5.

Создадим функцию BuildList(), в которой через цикл заполним колонку созданными выше ListItem.


В качестве ребенка документа вызовем BuildList() функцию. Разделение дерева из нод на функции не дает ему превратиться в макаронного монстра.


Если будет ситуация что надо выполнить какую-то логику прямо в дереве, то для того есть класс Builder. Но так лучше не делать...

После тела класса вызовем функцию Diplay(), которая при переходе на данный файл, на сайте cгенерирует его и выведит.


class ListDisplay extends DisplayComponent{  function Build() : Document {    return Document::Create()    // название страницы    ->Title("test page")    ->Child($this->BuildList());  }  function BuildList() {    $column = new Column;    for($i = 0; $i < 10; $i++)      $column->Children(        ListItem::Create()        ->Text("number: $i")      );    return $column;  }} ListDisplay::Display();

Макеты


Одна из целей преследуемых при создании фреймворка удобство при создание макетов сайта.


Container


Container это макет, который используется для декоративных целей.


Может содержать только одного ребенка.


Column и Row

У большинства элементов есть дети. Если у элемента доступен метод Child то он может иметь только одного ребенка, а если Children то у него может быть больше одного ребенка.


Так же Child перезаписывает переменную ребенка в то время как Children добавляет аргументы в верх стека.


В Children если один элемент в аргументе то не обязательно его заносить в массив.

Тоесть вместо Children([Text::Create()]) можно написать Children(Text::Create())

Column это макет который выравнивает его детей вертикально.


Обращаясь к функциям CrossAlign и MainAlign можно выравнивать детей внутри колонки.



Row идентичен Column, но выравнивает детей горизонтально.



Stack

Stack это макет дети которого не позиционированы. Это полезно если вы хотите чтобы ноды пересекались, для создания более красивого дизайна.



ScrollView, HorizontalScrollView, VerticalScrollView


Эти контейнеры который является областью для скороллинга.


HorizontalScrollView в этом контейнере можно скроллить только по горизонтальной оси.


VerticalScrollView в этом контейнере можно скроллить только по вертикальной оси.


ScrollView в этом контейнере можно скроллить по всем осям.


Стилизация


Долга думая как лучше встроить css в фреймворк я пришел к идее с константами.


Например у нас в css есть background-color. Я строку записываю в константу и в php коде можно будет использовать без "". Это намного удобнее.


...const BackgroundBlendMode = "background-blend-mode";const BackgroundAttachment = "background-attachment";const Border = "border";const BorderSpacing = "border-spacing";const BorderRadius = "border-radius";const BorderImage = "border-image";...

Что качается например такой конструкции "34".Px. Тут идея с константами выглядит не читабельно. По этому я решил в таких ситуациях использовать функции для css например Px(34). Выглядит понятно и вписывается в пхп код.


Простая стилизация

Для простой стилизации в элементе функция ThemeParameter(...). Первый аргумент это название параметра, а второй аргумент это или массив из значений/значение.


Рассмотрим пример.


В первом параметре мы изменим цвет фона на #f0f0f0.


В втором параметре мы добавим отступы. Сверху и снизу 20px, справа и слева 15px.


Значения в массиве генерируются через пробел. Если вы хотите чтобы генерация происходила через запятую, то для етого есть функция CommaLine().

...Container::Create()->ThemeParameter(BackgroundColor, Hex("f0f0f0"))->ThemeParameter(Padding, [Px(20), Px(15)]);...

Как видно все очень просто и удобно, но если нам понадобятся модификаторы (hover например)? Для этого сделаны темы.


Темы

Темы в этом Фреймворк это более продвинутый css, с media, keyframes, и модификаторами.


Напишем тему для контейнера с модификатором hover и active.


Для того надо создать класс темы и добавить в него ThemeBlock через метод ThemeBlocks.


Блоку темы нужно присвоить ключ / ключи. Я назову ключ my_container.


Дальше в блок темы можно добавить модификаторы. Я добавил: StandartModifier, HoverModifier, ActiveModifier. И задал для них параметры тебя через метод Parameter(...). Parameter работает так же как ThemeParameter.


function GetMyTheme() : Theme {  return Theme::Create()  ->ThemeBlocks([    ThemeBlock::Create()    ->Keys("my_container")    ->Modifiers([      StandartModifier::Create()      ->Parameter(BackgroundColor, Red)      ->Parameter(Padding, [Px(10), Px(12)]),      HoverModifier::Create()      ->Parameter(BackgroundColor, Green),      ActiveModifier::Create()      ->Parameter(BackgroundColor, Blue)    ])  ]);}

Дальше контейнеру я присвоил ключ (имя класса в css) темы через метод ThemeKeys. Но для того чтобы тему можно было использовать ее надо добавить в документ через метод Themes.


class TestThemeDisplay extends DisplayComponent{  function Build() : Document {    return Document::Create()    ->Themes(GetMyTheme())    ->Child(      Container::Create()      ->ThemeKeys("my_container")    );  }} TestThemeDisplay::Display();

После можно запустить дисплей и увидеть что тема по ключу применилась.


Продвинутые анимации

Для продвинутой анимации есть keyframes.


Для того чтобы добавить в тему keyframe, используйте метод FrameBlocks.


Добавим уже в существующую тему FrameBlock.


В FrameBlock есть метод Frames. Вызовем его и добавим несколько фреймов, так же для каждого фрейма надо указывать Value. Оно может быть в процентах (используйте функцию Pr(value)) или может быть константа From, To.


Каждому кадру можно добавлять параметры которые будут уникальны и плавно изменятся между другими кадрами.


Так же добавим блок темы в котором будет применяться эта анимация и назовем его shake_text.


Создадим текст в предыдущем дисплее, в контейнере и применим ему ключ темы.


Так же на примере видно что в параметре можно использовать и обычные строки.


function GetMyTheme() : Theme {  return Theme::Create()  ->ThemeBlocks([    ThemeBlock::Create()    ->Keys("my_container")    ->Modifiers([      StandartModifier::Create()      ->Parameter(Padding, [Px(10), Px(12)]),      HoverModifier::Create()      ->Parameter(BackgroundColor, Green),      ActiveModifier::Create()      ->Parameter(BackgroundColor, Blue)    ]),    ThemeBlock::Create()    ->Keys("shake_text")    ->Modifiers([      StandartModifier::Create()      ->Parameter(Color, Red)      ->Parameter(Animation, ["shake_text_anim", ".2s", "ease-in-out", "5", "alternate-reverse"])    ])  ])  ->FrameBlocks(    FrameBlock::Create()    ->Key("shake_text_anim")    ->Frames([      Frame::Create()      ->Value(Pr(0))      ->Parameter(Transform, Translate(0, 0)),      Frame::Create()      ->Value(Pr(25))      ->Parameter(Color, Hex("ff4040"))      ->Parameter(Filter, Blur(Px(0.5))),      Frame::Create()      ->Value(Pr(50))      ->Parameter(Filter, Blur(Px(1.2))),      Frame::Create()      ->Value(Pr(75))      ->Parameter(Color, Hex("ff4040"))      ->Parameter(Filter, Blur(Px(0.5))),      Frame::Create()      ->Value(Pr(100))      ->Parameter(Transform, Translate(Px(10), 0)),    ])  );}

class TestThemeDisplay extends DisplayComponent{  function Build() : Document {    return Document::Create()    ->Themes(GetMyTheme())    ->Child(      Container::Create()      ->ThemeKeys("my_container")      ->Child(        Text::Create()        ->ThemeKeys("shake_text")        ->Text("Error text")      )    );  }} TestThemeDisplay::Display();


Адаптивность

Создадим еще 2 темы. Одна будет для мобильных девайсов, а вторая для пк. У темы есть функции: MinWidth, MaxWidth, MinHeight, MaxHeight, объявив которые вы можете указать на каком размере будет работать тема.


Теме для телефонов зададим MinWidth 800px.


Теме для пк зададим MaxWidth 800px.


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


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


Добавим ключи темы к контейнеру.


function GetMobileTheme() : Theme {  return Theme::Create()  ->MinWidth(Px(800))  ->ThemeBlocks(    ThemeBlock::Create()    ->Keys("adaptive_color")    ->Modifiers(      StandartModifier::Create()      ->Parameter(BackgroundColor, Green)    )  );}

function GetDesktopTheme() : Theme {  return Theme::Create()  ->MaxWidth(Px(800))  ->ThemeBlocks(    ThemeBlock::Create()    ->Keys("adaptive_color")    ->Modifiers(      StandartModifier::Create()      ->Parameter(BackgroundColor, Red)    )  );}

class TestThemeDisplay extends DisplayComponent{  function Build() : Document {    return Document::Create()    ->Themes([      GetMyTheme(),       GetDesktopTheme(),       GetMobileTheme()    ])    ->Child(      Container::Create()      ->ThemeKeys(["my_container", "adaptive_color"])      ->Child(        Text::Create()        ->ThemeKeys("shake_text")        ->Text("Error text")      )    );  }} TestThemeDisplay::Display();


Логика


Попробуем написать простой кликер.


Для начала нам надо создать класс и наследовать его от DisplayComponent.


Создадим функцию Build() и возвратим в ней Document.


class ClickerDisplay extends DisplayComponent{  function Build() : Element {    return Document::Create()    ->Title("Clicker");   }} ClickerDisplay::Display();

Добавим колонку в качестве ребенка документа.


Так же в качестве детей колонки добавим текст и кнопку.


class ClickerDisplay extends DisplayComponent{  function Build() : Element {    return Document::Create()    ->Title("Clicker")    ->Child(      Column::Create()      ->Children([        Text::Create()        ->Text("Pressed 0 times"),        Button::Create()        ->Text("Press")      ])    );   }} ClickerDisplay::Display();

Результат будет следующим.



Далее добавим простые ThemeParameter, чтобы сделать наш пример красивее.


class ClickerDisplay extends DisplayComponent{  function Build() : Element {    return Document::Create()    ->Title("Clicker")    ->Child(      Column::Create()      ->ThemeParameter(Padding, Px(15))      ->Children([        Text::Create()        ->ThemeParameter(PaddingBottom, Px(15))        ->Text("Pressed 0 times"),        Button::Create()        ->ThemeParameter(Width, Auto)        ->ThemeParameter(Padding, [Px(4), Px(10)])        ->ThemeParameter(BackgroundColor, Blue)        ->ThemeParameter(Color, White)        ->ThemeParameter(BorderRadius, Px(4))        ->Text("Press")      ])    );   }} ClickerDisplay::Display();

Выглядит куда лучше в несколько простых строчек.



Теперь можно добавить логику.


Для начала нужно инициализировать функцию Initialize() и создать приватную переменную TapCount.


Аналог form в фреймворке это Action.

Добавим Action в наше дерево элементов. Action тип пусть будет Post. В качестве детей укажем нашу колонку где находится наша кнопка.


Далее добавим click_count переменную в Action. А в качестве ее значение присвоим TapCount.


В Initialize() через Action::GetValue(name, standart_value, action_type) получим наше переменную. В качестве значения по умолчанию укажем 0, а в качестве типа укажем Post.


Добавим инкремент для нашей переменной.


В тексте выведим "Press $this->TapCount times".


Все, простой клинкер готов.


class ClickerDisplay extends DisplayComponent{  private $TapCount;  function Initialize() {    $this->TapCount = Action::GetValue("click_count", 0 /* standart value */, ActionTypes::Post);    $this->TapCount++;  }  function Build() : Document {    return Document::Create()    ->Title("Test page")    ->Child(      Action::Create()      ->Type(ActionTypes::Post)      ->Variable("click_count", $this->TapCount)      ->Child(        Column::Create()        ->ThemeParameter(Padding, Px(15))        ->Children([          Text::Create()          ->ThemeParameter(PaddingBottom, Px(15))          ->Text("Press $this->TapCount times"),          Button::Create()          ->ThemeParameter(Width, Auto)          ->ThemeParameter(Padding, [Px(4), Px(10)])          ->ThemeParameter(BackgroundColor, Blue)          ->ThemeParameter(Color, White)          ->ThemeParameter(BorderRadius, Px(4))          ->Text("Press")        ])      )    );  }} ClickerDisplay::Display();


Итог


Мне удалось написать простой, но достаточно мощный кодогенератор.


Он прошол путь от генерации простых таблиц до полноценного генератора html и css, на котором можно удобно верстать проекты и совмещять верстку с логикой.


На данном фреймворке я написал курсовой проект (скриншоты ниже), использовал его на экзамене и делал на нем учебные задания.


Скриншоты курсового проекта сделанного на MelonPHP




Источники


GitHub MelonPHP


Flutter


MAUI

Подробнее..
Категории: Php , Ui , Framework , Php7 , Generators , Codegeneration

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru