Pull to refresh

Reinforced.Typings — больше деталей

Reading time17 min
Views8.9K
И снова здравствуйте.
Кто о чем, а я снова о Reinforced.Typings — своей библиотеке для генерации TypeScript-ового glue-кода из C#-сборок, краткое введение в которую я сделал в предыдущем посте. После чего немедленно получил ряд вопросов и комментариев (не только на Хабрахабре, к слову — многие из заинтересовавшихся просто на нем не зарегистрированы). За это, конечно, всем огромное спасибо, но исходя из проанализированной информации, я понял что одного краткого поста не достаточно, чтобы описать как и что реализуется. Получается что люди задают вопросы «а вот это вот поддерживается?» и каждому приходится объяснять раз за разом, одно и то же. Так что в этой статье я сделаю небольшой cheatsheet по атрибутам, fluent-конфигурации и расскажу про дополнительные возможности. В общем, милости просим. Осторожно, лонгрид и справочная информация!


В предыдщей серии


Когда я впервые рассказывал про Reinforced.Typings, я вскользь упомянул, что есть разные атрибуты и у них много всяких-разных кастомных свойств, есть какие-то кодогенераторы и что-то про процесс сборки TypeScript. Все как-то сумбурно, поверхностно, без подробностей. Давайте разложим по полочкам. Я максимально сжато, но детально расскажу про различные варианты конфигурации, сборочный скрипт, как оно все вместе работает и что из этого всего можно вылепить.
В общем и целом эта статья — своеобразный manual/методичка/cheatsheet по Reinforced.Typings, потому как у меня все еще нет времени и ресурса написать полноценную документацию на английском. Конечно, не все так печально — я уже собрался с духом и написал ажно целый план документации. Однако на большее (+ статью на Хабрахабре) «отца русской демократии» не хватило. Итак, поехали.
P.S.: Стоит отметить, что я немного все-таки считерил и за время, которое прошло с момента написания последней статьи, допилил некоторые возможности в библиотечке, починил баги (куда уж без них). По сему, излагаемый материал актуален с версии 1.0.7.

Как оно в целом работает


Вы расслаблены, довольны, у вас теплые ноги и вы пишете на C# и ASP.NET MVC с использованием TypeScript. Вы устанавливаете Reinforced.Typings из NuGet командой

PM > Install-Package Reinforced.Typings

Reinforced.Typings сразу же встраивается в процесс сборки вашего приложения, запускаясь при каждом билде проекта. Этот момент контролируется из файла Reinforced.Typings.settings.xml, который добавляется в корень вашего проекта — я называю это «сборочная конфигурация». Запустившись, Reinforced.Typings делает свое темное дело, подгружая сборку вашего проекта и залезая в нее через Reflection. Желанный TypeScript glue-код (например, TypeScript-интерфейсы соответствующие вашим серверным TransferObjects/View-моделям, передаваемым на клиент) генерируется основываясь на двух возможных факторах:
  • Атрибутной конфигурации — то есть экспортируются типы и их члены, помеченные специальными атрибутами (их список и свойства будут даны ниже)
  • Fluent-конфигурации — когда для выяснения как и что вы хотите сгенерировать вызывается метод, указанный в сборочном конфиге. Этот вариант конфигурации так же будет освещен далее

Атрибутную и fluent-конфигурацию можно комбинировать — это не возбраняется. Они в основном дают равные возможности, но различаются по гибкости.
В общем-то, все тайные знания, необходимые для использования Reinforced.Typings, заключаются в том, чтобы ориентироваться в параметрах его конфигурации. И тут всего 3 точки настройки, упомянутые выше — сборочная конфигурация (Reinforced.Typings.settings.xml), атрибутная и fluent-конфигурация. Сборочная конфигурация определяет глобальные параметры всего процесса вроде того, в какие файлы будет записан сгенерированный код, экспортировать ли документацию, какие сборки подключать и т.п. Атрибутная и fluent-конфигурации определяют какой именно код генерировать и для каких типов.

Сборочная конфигурация


Reinforced.Typings генерирует ваши тайпинги при каждой пересборке проекта. Как было оговорено выше, он встраивается в процесс сборки проекта. Делается это посредством посредством мехнизма встраивания .targets и .props-файлов из пакета, который появился в версии NuGet 2.5.
К слову
Сам генератор вынесен в инструмент rtcli.exe, который запускается посредством входящей в комплект MSBuild-таски RtCli. Словом, все в лучших традициях вызывания кастомных инструментов во время билда. В принципе вас ничто не останавливает от наглого выдирания rtcli.exe и Reinforced.Typings.dll из директории /tools самого пакета и использования по своему усмотрению (равно как и таски RtCli, которая лежит в /build/Reinforced.Typings.Integrate.dll), но давайте будем объективны — мало кто реально будет это делать.

Некоторые параметры запуска генератора вынесены в файл Reinforced.Typings.settings.xml, кой являет собой кусок MSBuild-скрипта, подключающийся в .targets-файле самого пакета. Reinforced.Typings.settings.xml добавляется в корень вашего проекта при установке пакета. По умолчанию он выглядит вот так вот. Я бы не стал утверждать, что он хорошо документирован (в основном по причине ломанности своего английского), поэтому привожу табличку-cheatsheet с описанием параметров конфигурации:
Параметры в Reinforced.Typings.settings.xml
RtTargetFile (cтрока)
Полный путь до файла, в который будут записаны все сгенерированные тайпинги. В нем (как и во всех других параметрах) можно использовать переменные MSBuild, в том числе специфичные для Microsoft.Common.CurrentVersion.targets. Этот параметр является ключевым и обязательным, когда вам просто надо скинуть тайпинги для вашего проекта в один файл, однако он не используется, если включена функция разбиения сгенерированного кода по многим файлам (см. ниже). Не забудьте добавить этот файл в свой проект руками!

RtConfigurationMethod (cтрока)
Задает полное имя метода, который будет вызван для построения fluent-конфигурации. Например My.Assembly.Configuration.ConfigureTypings. Сборку указывать не нужно — Reinforced.Typings сам найдет этот метод или в сборке самого проекта, или в его references. Метод должен быть статическим и принимать на вход единственный параметр типа Reinforce.Typings.Fluent.ConfigurationBuilder (название параметра не важно). Стоит отметить, что у вас по-прежнему есть возможность использовать атрибуты, даже если вы используете fluent-конфигурацию. Однако имейте в виду, что если для одного и того же члена есть и fluent и атрибутная конфигурация, то предпочтение будет отдано fluent.

RtWriteWarningComment (true/false)
Контролирует записывание в выходной файл предупреждалки о том, что файл сгенерирован автоматически. Если этот параметр установлен в true, то в заголовке каждого сгенерированного файла будет красоваться надписть "// This code was generated by a Reinforced.Typings tool. " бла-бла-бла. Если честно, я не знаю кому может пригодиться этот параметр, но он есть.

RtExportPureTypings (true/false)
Заставляет RT генерировать TypeScript-овые тайпинги (.d.ts) вместо обычного TypeScript. Включение этого параметра конфигурации заставляет библиотеку оправдывать свое название (хотя по умолчанию от отключен). А дело все в том, что синтаксис .d.ts и .ts несколько различается. Отсюда и необходимость существования этого параметра.

RtDivideTypesAmongFiles (true/false)
Когда этот параметр false, то все сгенерированные тайпинги будут записаны в один файл, указанный в параметре RtTargetFile. Это не всегда удобно, ибо может привести, например, к монструозным мерджам при использовании SCM. Если установить этот параметр в true, то сгенерированный TypeScript будет раскидан по разным файлам (class-per-file). При этом RtTargetFile будет проигнорирован. Только не забудьте добавлять сгенерированные файлы в свой проект руками по мере появления!
Вы можете настроить что и куда положить с помощью атрибута [TsFile]/fluent-вызова .ExportTo. Стоит отметить, что RT сам прекрасно разберется с добавлением директив ///<reference ...> на соседние используемые типы в этом случае. Но при затруднении вы всегда можете помочь ему с помощью атрибута [TsAddTypeReference]/fluent-вызова .AddReference

RtTargetDirectory (строка)
Используется совместно с RtDivideTypesAmongFiles для указания директории, в которую будут свалены все сгенерированные файлы. Обязательно укажите этот параметр, если вы используете RtDivideTypesAmongFiles.

RtRootNamespace (строка)
Так же используется совместно с RtDivideTypesAmongFiles. Дело в том, что через Reflection невозможно определить корневое пространство имен сборки. А без этого не удастся правильно раскидать сгенерированные файлы по директориям.

RtBypassTypeScriptCompilation (true/false)
TypeScript собирается первым при сборке проекта. Иногда возникает ситуация, что вы сгенерировали тайпинги, а они сделали TypeScript-код вашего проекта несобираемым. И чтобы починить это, вам нужно пересобрать проект и перегенерировать тайпинги, однако вы не можете этого сделать, так как проект не собирается из-за того, что не собираются тайпскрипты. Выйти из этого порочного круга позволяет установка RtBypassTypeScriptCompilation в true. Эта настройка отключает сборку тайпскриптов перед сборкой проекта и собирает их после сборки проекта, что дает .dll-ке вашего проекта преспокойно собраться, а RT — сгенерить из нее свежие тайпинги. Пожалуйста, не забудьте вернуть этот параметр в false, когда проблема устранится. Иначе возможны проблемы с паблишингом собранных javascript-ов.

RtCamelCaseForMethods (true/false) (реализован фич-реквест Tremor)
Принудительно преобразоывает названия всех методов в camelCase (вместо традиционного для .NET PascalCase). Используется эстетами от javascript. Так же camelCase-инг можно контролировать отдельно для каждого метода путем использования свойства ShouldBeCamelCased атрибута TsFunction.

RtCamelCaseForProperties (true/false) (реализован фич-реквест Tremor)
То же самое что RtCamelCaseForMethods, только для свойств. ShouldBeCamelCased так же есть у TsProperty.

RtGenerateDocumentation (true/false)
При установке в true, а так же включении генерации документации в XML в настройках вашего проекта, Reinforced.Typings вытащит путь к файлу с XMLDOC-ом в ходе процесса сборки и преобразует его в jsdoc, который и допишет в генерируемые файлы. В этом подходе все прекрасно, за исключением того что по умолчанию, после включения в проекте экспорта XML-документации, компилятор начинает засыпать вас ворнингами для незадокументированных публичных классов/членов класса. Не то чтобы это как-то мешало технически, но визуально бесит.

RtDisable (true/false)
Отключает Reinforced.Typings. Пока этот параметр true — процедура генерации тайпингов вызываться не будет. Однако, RtBypassTypeScriptCompilation по-прежнему будет активен.

Item-группа RtAdditionalAssembly
В эту Item-группу можно скинуть дополнительные сборки, которые Reinforced.Typings должен принять во внимание при экспорте (читай: экспортировать тайпинги и из них тоже). В распоряжении RT есть полные пути к References вашего проекта, поэтому в RtAdditionalAssembly можно включить просто имя сборки (все, что стоит до .dll).


Атрибуты


Одной сборочной конфигурации для успешного экспорта недостаточно. Вам так же придется указать RT что конкретно вы хотите видеть в сгенерированных TypeScript-файлах. Сделать это можно, например, повесив соответствующие атрибуты над экспортируемыми типами (классами, интерфейсами, перечислениями) и их членами. Это я называю «атрибутная конфигурация». Есть еще fluent-конфигурация, но о ней я расскажу чуть позже, потому как она полностью базируется на атрибутной и во многом её повторяет, а по сему — будет педагогичнее рассказать сначала про атрибутную конфигурацию.
Все атрибуты Reinforced.Typings лежат, как ни странно, в пространстве имен Reinforced.Typings.Attributes, что может сбить новичка с толку (sarcasm). От всех атрибутов можно унаследоваться. Все свойства перегружаются. Как одна из доступных техник работы — вы можете унаследоваться от любого из них и сделать свой атрибут, чтобы не таскать за собой пачку параметров каждый раз.
Ниже я привожу cheatsheet по всем доступным атрибутам. Выражение «обязательный атрибут» в нем означает, что если вы этот атрибут не поставите, то экспортироватся в TypeScript соответствующая сущность не будет.

Название атрибута Обязателен? Результат в тайпинге Экспортируемый (пациент)
TsInterface Да TypeScript-интерфейс Класс, интерфейс, структура
TsClass Да TypeScript-класс Класс, структура
TsProperty Нет Поле интерфейса/класса Property, поле (класса/структуры)
TsFunction Нет Метод интерфейса/класса (в случае с классом — тело экспортируется как return null; если метод не-void и как пустое в случае void) Property, поле (класса/структуры)
TsEnum Да TypeScript-перечисление enum, очевидно
TsValue Нет Одно из значений TypeScript-перечисления Значение enum, неочевидно
TsParameter Нет Параметр (формальный аргумент) TypeScript-метода Параметр метода, как ни странно
TsGeneric Нет Тип-параметр TypeScript-метода/класса/whatever Тип-параметр
TsIgnore Нет Пациент не будет экспортироваться в TypeScript Свойство, конструктор (!), поле, метод, параметр

Немного об экспорте конструкторов
Отдельно хочется упомянуть атрибут TsBaseParam. RT может экспортировать классы, а так же их конструкторы. В случае, когда вы наследуете классы друг от друга и явно вызываете конструктор предка в конструкторе наследника с использованием :base() — никакой информации об этом через Reflection получить нельзя. Поэтому для корректного экспорта таких случаев, вы можете поставить над конструктором атрибут TsBaseParam. У него один конструктор и принимает он на вход массив строк, в которых вы можете писать любые TypeScript-выражения. Это все будет записано в TypeScript-овый super(...). На практике я слабо представляю себе полезность этого атрибута, но он есть. Есть мнение что этот атрибут существует просто чтобы показать насколько крут Reinforced.Typings.

В остальном, если вы включите экспорт конструкторов путем соответствующего свойства атрибута [TsClass], то они будут экспортированы корректно. То есть, никаких специальных атрибутов для конструкторов не предусмотрено.


Свойства атрибутов


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

Портянка текста
TsInterface

  • AutoI (true) — ставить ли автоматически букву I перед именем пациента (если её нет)
  • AutoExportMethods (true) — экспортировать ли автоматически все методы пациента. Если нет — будете ставить [TsFunction] над методами сами
  • AutoExportProperties (true) — то же, что и выше, но для пропертей и [TsProperty]
  • IncludeNamespace (true) — помещать ли пациента в module при экспорте
  • Name (null) — переопределяет имя пациента
  • Namespace (null) — переопределяет неймспейс пациента

TsClass

  • AutoExportProperties, AutoExportMethods, IncludeNamespace, Name, Namespace — аналогично свойствам TsInterface. Плюс:
  • AutoExportFields (true) — то же, что AutoExportProperties, но для полей
  • DefaultMethodCodeGenerator (null) — позволяет переопределить кодогенератор сразу для всех экспортируемых методов класса

TsProperty

  • Type (null, подставляется имя типа) — переопределяет имя типа пациента в TypeScript. Как строку. Ну то есть можно написать вообще что угодно. Помогает, когда тип свойства не выводится в TS (вы получаете any), или надо сделать ссылку на неэкспортируемый тип. Например — на JQuery
  • StrongType (null, подставляется имя типа) — то же, что и Type, но вы можете указать .NET-овский тип. Удобно для делегатов например — написал StrongType = typeof(Func<int,bool>) и порядок, не надо париться с тоннами скобочек
  • Name (null) — переопределяет имя пациента
  • ForceNullable (false) — говорит принудительно сделать поле nullable. Ну то есть field:boolean превратить в field?:boolean
  • ShouldBeCamelCased (false) — должно ли имя свойства быть преобразовано в camelCase (реализован фич-реквест Tremor)

TsFunction

  • Type, StrongType — аналогично свойствам TsProperty, но переопределяет возвращаемый тип метода
  • Name (null) — переопределяет имя пациента
  • ShouldBeCamelCased (false) — должно ли имя метода быть преобразовано в camelCase (реализован фич-реквест Tremor)

TsEnum

  • IncludeNamespace, Name, Namespace — аналогично свойствам TsInterface. Более параметров не имеет.

TsValue

  • Name — переопределяет имя пациента (конкретного значения enum-а то есть)

TsParameter

  • Type, StrongType — аналогично свойствам TsProperty, но переопределяют тип аргумента метода
  • Name (null) — переопределяет имя пациента
  • DefaultValue (null) — указывает значение по умолчанию. Чтобы подставлялось как parameter:boolean = false, например. Осторожно, тут можно выстрелить себе в ногу.
  • ShouldBeCamelCased (false) — должно ли имя параметра быть преобразовано в camelCase (реализован фич-реквест Tremor)

TsGeneric

  • Type, StrongType — аналогично свойствам TsProperty, но переопределяют тип тип-параметра

TsBaseParam

  • Values — массив строк, предствляющих собой TypeScript-выражения. Их содержимое которых будет использовано для генерации вызова super(...) в случае экспорта TypeScript-класса

TsAddTypeReference

  • RawPath — путь к файлу, который будет записан в директиве ///<reference ...>, которая будет добавлена в файл с экспортируемым типом
  • Type — тип, путь к файлу, содержащему который, будет добавлен в директиве ///<reference ...> в файл с экспортируемым типом


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

Экспорт TypeScript-кода в несколько файлов


Эта возможность включается при установке RtDivideTypesAmongFiles в true в сборочном конфиге. По умолчанию, без дополнительной конфигурации, RT раскидает все ваши классы согласно старой-доброй традиции ОО-языков — по классу в отдельный файл, да еще и положит в поддиректории согласно Namespace-ам (чтобы не наплодить в связи с этим лишних директорий, рекомендуется использовать параметр сборочного конфига RtRootNamespace). Так же есть один нюанс — студия не всегда видит, что какой-либо из используемых в .ts-файле типов лежит в соседнем файле и подчеркивает красным почем зря. Чтобы такой ситуации не возникало — во все .ts-файлы нужно добавить директиву ///<reference path="..." >. Так вот — в большинстве случаев с корректным размещением этой директивы RT справляется сам, инспектируя зависимости типов путем просматривания типов свойств/полей, возвращаемых значений методов, типов параметров и т. п. И это бесценно. Для остального есть MasterCard атрибут TsAddTypeReference. В спойлере ниже речь пойдет о нем, а так же о некоторых других атрибутах, которые влиялют на раскидывание TypeScript-кода по разным файлам.

Еще одна портянка текста
  • TsAddTypeReference — этот атрибут позволяет добавить директиву ///<reference ...> в файл, в который будет записан сгенерированный код для пациента. Это необходимо когда вы, например, переопределяете Type строкой для каких-либо экспортируемых членов и проинспектировать референсы через Reflection не представляется возможным. Как параметр конструктора этого атрибута можно указать тип (например [TsAddTypeReference(typeof(AnotherExportedType))]). Тогда RT сам определит в каком файле он лежит и сгенерирует для директивы соответствующий путь. В случае указания явного пути (например [TsAddTypeReference("../../jquery.d.ts")]), именно он будет использован для добавления директивы. Этот атрибут вешается на классы, интерфейсы и enum-ы. Допускается использовать несколько раз.
  • TsFile — Указывает в какой файл положить сгенерированный код для пациента. Этот атрибут активен только когда RtDivideTypesAmongFiles в сборочной конфигурации установлен в true. Путь в этом атрибуте указывается относительно RtTargetDirectory
  • TsReference — Раз уж мы заговорили об обработке референсов, то надо упомянуть этот атрибут. Он указывает что в сгенерированный файл (или все файлы) надо добавить директиву ///<reference ...> на указанный путь. Без привязки к какому-то конкретному типу. Референс, указанный этим атрибутом будет добавлен во каждый сгенерированный файл. Полезно для добавления референса, например, на тайпинг для jQuery. Этот атрибут вешается на сборку, следовательно, писать его надо как [assembly: TsReference("~/Scripts/typings/jquery.d.ts")]. Так же допускается использовать его несколько раз.




CodeGenerator


Почти все атрибуты унаследованы от TsAttributeBase, который определяет всего одно свойство — CodeGeneratorType типа Type. Что это такое? А это возможность указать тип кодогенератора для любого члена класса или типа. Кодогенератор — это специальный класс, который должен реализовывать интерфейс Reinforced.Typings.Generators.ITsCodeGenerator<>, параметризованный:
  • System.Type в случае кодогенератора для типа (используется с TsEnum, TsClass, TsInterface)
  • System.Reflection.PropertyInfo в случае кодогенератора для property (используется с TsProperty)
  • System.Reflection.FieldInfo в случае кодогенератора для поля (используется с TsField)
  • System.Reflection.MethodInfo в случае кодогенератора для метода (используется с TsFunction)
  • System.Reflection.ParameterInfo в случае кодогенератора для параметра метода (используется с TsParameter)

ITsCodeGenerator<> определяет всего одно свойство (Settings), которое достаточно реализовать как auto-property и всего один метод — Generate, который принимает на вход:
  • TElement, которым и параметризован генератор — сущность, для которой надо сгенерировать TypeScript — код
  • TypeResolver — класс, самым ценным методом которого является метод ResolveTypeName, принимающий на вход тип. Он возвращает TypeScript-friendly имя типа, которое можно писать в генерируемый код.
  • WriterWrapper — небольшая обертка над TextWrapper-ом, в который нужно записать генерируемый код
  • Так же вы можете использовать свойство Settings внутри метода Generate для того, чтобы, например, посмотреть (или записать в выходной поток) документацию для генерируемой сущности

В RT есть несколько готовых генераторов для классов, интерфейсов, enum-ов, параметров полей, методов и property — они все лежат в пространстве имен Reinforced.Typings.Generators и от них можно наследоваться. Конечно же, ничто не мешает сделать свой кодогенератор напрямую реализовав интерфейс ITsCodeGenerator<>.
Когда вы сделали свой кодогенератор, нужно указать что нужно использовать именно его, установив свойство CodeGeneratorType соответствующего атрибута в typeof от вашего генератора. Никакого контроля типобезопасности тут, к сожалению нет. Ну то есть если вы укажете CodeGeneratorType = typeof(string), то IntelliSense не скажет что что-то не так. Однако, эта же функциональность предоставляется во fluent-конфигурации (метод .WithCodeGenerator<>), и вот там уже указать неподходящий кодогенератор вам не дадут.

А вот например
..., в моем текущем проекте, я унаследовался от Reinforced.Typings.Generators.MethodCodeGenerator, сделав свой генератор ActionInvokeGenerator, который пишет glue-code для вызова метода MVC WebAPI и возврата promise. После этого пометил нужные контроллеры, методы которых надо пробросить в TypeScript атрибутом [TsClass(AutoExportMethods = false)], а сами методы — с [TsFunction(CodeGeneratorType = typeof(ActionInvokeGenerator))]. В результате получил обертку для интересующих меня методов на клиенте. Об этом замечательно опыте я расскажу в следующей статье.


Fluent-конфигурация


Размещать атрибуты над экспортируемыми типами не всегда удобно. Например, в случае, когда вам надо экспортировать сотню типов из определенного namespace-а, при том выдрать из них определенные свойства и прописать для них определенную конфигурацию экспорта, то размещать атрибуты становится делом несколько скучным и монотонным. К тому же, вы не сможете разместить атрибуты экспорта над сущностями, к исходному коду которых у вас нет доступа. Как раз чтобы побороть это обстоятельство, в версии 1.0.5 была добавлена вомзожность fluent-конфигурации. Начать использовать её наряду с атрибутами можно в 2 простых шага:
  1. Создайте отдельный класс, объявите в нем публичный статический метод с единственным параметром типа Reinforced.Typings.Fluent.ConfigurationBuilder. Например, вот так:
    using Reinforced.Typings.Fluent;
    namespace TestProject.App_Start
    {
        public class TypingsConfiguration
        {
            public static void ConfigureTypings(ConfigurationBuilder builder)
            {
                // тут будет ваша fluent-конфигурация
            }
        }
    }
    

  2. В сборочной конфигурации укажите используемый fluent-метод. Например, так:
    <RtConfigurationMethod>TestProject.App_Start.TypingsConfiguration.ConfigureTypings</RtConfigurationMethod>
    


Всё. Теперь этот метод будет вызываться каждый раз при перегенерации тайпингов (читай: при пересборке проектов) и готовить конфигурацию экспорта. Через fluent-конфигурацию достаточно легко можно экспортировать пачками однотипные интерфейсы и классы с однотипной же конфигурацией (реализован фич-реквест Tremor, пусть и немного в другом виде), что облегчает задачу в случае большого проекта. Так же несомненным плюсом, на мой взгляд, является строго типизированное указание кодогенератора, благодаря которому вы уже никогда по ошибке не примените кодогенератор для метода на свойствах и наоборот.

С вашего позволения, я не буду детально расписывать все методы ConfigurationBuilder-а (и всех вложенных билдеров), ибо как они интуитивно понятны, а так же имеют хороший XMLDOC. Приведу лишь небольшой кусок кода fluent-конфигурации, чтобы создать представление как примерно она выглядит:
Кусок кода
using Reinforced.Typings.Fluent;
namespace TestProject.App_Start
{
    public class TypingsConfiguration
    {
        public static void ConfigureTypings(ConfigurationBuilder builder)
        {
            builder.ExportAsInterface<ILoginInformation>()
                .WithPublicProperties()
                .WithProperty(c => c.Email).Type<int>();

            builder.ExportAsInterface<ILoginPage>()
                .WithPublicProperties()
                .WithPublicMethods()
                .WithMethod(c => c.FillIn(Ts.Parameter<ILoginInformation>(o => o.Type<string>())))
                .Returns<string>();
            builder.ExportAsInterface<ILoginPage>()
                .WithMethod(c => c.AddOrders(Ts.Parameter<OrdersColelction>()));

            builder.ExportAsInterfaces(
                new[] {
                    typeof (ILoginPage), 
                    typeof (ILoginInformation),
                    typeof(IOrder)
                },
                c => c.ExportTo("login.ts").WithAllMethods(m => m.CamelCase()));
        }
    }
}


Я думаю, что читатель без труда поймет что делает эта конфигурация. Однако, не удержусь от пары ремарок.
Ремарка раз: TsBaseParam и TsGeneric остаются доступны только как атрибуты. Мне не удалось придумать красивый fluent-подход для конфигурирования этих моментов. С другой стороны я вполне заслуженно считаю вынос этого во fluent-конфигурацию в некотором роде оверкиллом. Словом — используйте атрибуты для настолько узких мест.
Ремарка два: в отдельном пояснении здесь нуждается Ts.Parameter и метод TryLookupDocumentationForAssembly.
Ts.Parameter — это специальный статический метод, который может принимать на вход построитель fluent-конфигурации для параметров метода. То есть когда вы хотите задать конфигурацию для параметра метода, вы просто говорите нечто вроде
builder.ExportAsInterface<MyClass>()
    .WithMethod(m => m.MyMethod(
            Ts.Parameter<int>(p => p.OverrideName("apple").Type<string>())
              ))
    .Returns<object>();

чтобы получить в TypeScript-е
export IMyClass {
	MyMethod(apple:string):any;
}

/*
При исходных данных
class MyClass
{
	public int MyMethod(int a)
	{
		return 0;
	}
}
*/

На мой взгляд, удобный и элегантный способ.
TryLookupDocumentationForAssembly — это метод самого ConfigurationBuilder-а, который говорит RT поискать файл с xml-документацией для указанной сборки рядом с самой сборкой. Имеет одну перегрузку, которая позволяет указать имя файла непосредственно (таким образом сборка будет использована только для определения директории, в которой надо искать). Я пришел к необходимости добавить этот метод, когда встала необходимость экспортировать документацию из другой сборки.
Вот. Теперь точно всё.

Заключение


Статья вышла довольно крупной, что, в принципе, соответствовало моей цели сделать более-менее подробный мануал при полном отсутствии документации по проекту и категорическом недостатке свободного времени для авторства. Еще меня не хватило на примеры использования, но им я посвящу следующую статью и постараюсь дописать и докоммитить примеры на github. Не стреляйте в пианиста — он пишет когда есть время.
Как обычно, я открыт к пожеланиям-предложениям и прочему feedback-у. Если в ходе использования вы найдете баги — не стесняйтесь отправлять issues на github проекта. Неожиданным pull-реквестам я так же очень рад — писать в одного, совмещая с fulltime-работой становится тяжеловато.
NuGet-пакет проекта лежит там же, где и лежал.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+16
Comments2

Articles