Skip to content

Commit 4237f71

Browse files
authored
[MOB-59338] Добавляет поддержку include-механики для виджетов (#128)
* [MOB-59261] Удаляет плагины hh-carnival и hh-garcon * [MOB-59261] Переносит контент всех shared-модулей в main sourceSet плагина hh-geminio * [MOB-59261] Удаляет остатки shared-модулей из проекта * [MOB-59261] Переносит исходники плагина Geminio в корень проекта, так как Geminio остался единственным плагином в репозитории * [MOB-59261] Переносит документацию Geminio ближе к корню проекта * [MOB-59261] Обновляет версию Gradle до 9.4.1 * [MOB-59261] Переносит настройку плагина в settings.gradle и корневой build.gradle — по аналогии с официальным шаблоном плагинов IntelliJ IDEA + удаляет ставшие ненужными build-logic и libraries модули, ненужные скрипты + обновляет run-конфигурации * [MOB-59261] Переносит оставшийся скрипт git-hook в новую папку * [MOB-59261] Немного уточняет .gitignore * [MOB-59261] Исправляет работу detekt и формирует актуальный baseline * [MOB-59261] Исправляет GitHub worflow после упрощения конфигурации * [MOB-59261] И ещё раз немного уточняет .gitignore * [MOB-59261] -- Обновляет версию Detekt в GitHub Action * [MOB-59261] -- Исправляет id плагина * [MOB-59318] Добавляет несколько тестов на текущее поведение плагина (поведение виджетов и исполнение команд) * [MOB-59318] Добавляет runtime для работы UI-формы, отвязанный от внутренностей Android Studio * [MOB-59318] Мигрирует построение UI шаблона Geminio на кастомный диалог внутри плагина + добавляет диалог для отображения процесса создания файлов * [MOB-59318] Мигрирует построение UI шаблона модулей Geminio на кастомный визард внутри плагина * [MOB-59318] Убирает ненужные фабрики диалогов после перехода на кастомные диалоги * [MOB-59318] Добавляет новый кастомный runtime для выполнения geminio-рецептов * [MOB-59318] Добавляет механику Undo для создания модулей и создания файлов * [MOB-59318] Убирает версию runtime, завязанную на шаблоны Android Studio, + исправляет работу modifier-функций, отвязывая их от реализации Android-плагина * [MOB-59318] Убирает вызовы TemplateUtils * [MOB-59318] Убирает вызовы android-плагина (TemplateUtils и ReformatUtils) из рантайма Geminio * [MOB-59318] Переносит adapter-слой вокруг android-плагина в отдельный пакет geminio/services + исправляет замечания detekt * [MOB-59318] Поднимает версию плагина * [MOB-59318] Исправляет замечания detekt в тестах * [MOB-59318] Исправляет замечания detekt в kts-скриптах * [MOB-59319] Добавляет поддержку suggest-параметров * [MOB-59319] Исправляет ссылку в одном из файлов документации * [MOB-59319] Дописывает изменения в CHANGELOG * [MOB-59338] Добавляет поддержку include-механики для виджетов * [MOB-59338] Исправляет замечания detekt * [MOB-59261] Исправляет замечание от ИИ — исправляет документацию по обновлению Geminio * [MOB-59261] Исправляет замечание от ИИ — добавляет проверку на существование release-tag при сборке на CI * [MOB-59318] Исправляет замечания от ИИ — возвращает поддержку валидации различных constraints строковых полей * [MOB-59318] Исправляет замечания от ИИ — возвращает обработку FreemarkerException * [MOB-59318] Убирает константу String.EMPTY * [MOB-59318] Исправляет замечание от ИИ — исправляет формирование путей и валидацию package в Geminio * [MOB-59319] Исправляет ошибки после merge предыдущей задачи * [MOB-59319] Исправляет замечания ИИ — добавляет защиту от дублирования значений в suggestParameter, убирает лишние IllegalArgumentException, переиспользует toBooleanRecipeExpression там, где это необходимо * [MOB-59338] Исправляет замечания ИИ — добавляет защиту от некорректных ключей внутри include-блока
1 parent fe6c1cb commit 4237f71

10 files changed

Lines changed: 472 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Added
66

77
- `suggestParameter` — a searchable string-backed parameter with optional `sealed` mode, inline options and `options.source` CSV support.
8+
- `include` in the `widgets` section to reuse shared widget groups across multiple recipes.
89
- Support `==` and `!=` operators for string-backed parameters in boolean expressions such as `visibility` and `availability`.
910
- Undo support for recipe execution actions.
1011

docs/en/RECIPE_CONTENT.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ widgets:
8787
options:
8888
source: options/modules.csv
8989

90+
- include:
91+
file: shared/codeowners.widgets.yaml
92+
9093
- stringParameter:
9194
id: moduleName
9295
name: Fragment Toothpick Module

docs/en/recipe_content/WIDGETS.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,35 @@ Additional notes:
111111
`default`, and if `default` is also missing or invalid, it falls back to the first option;
112112
- for `sealed: false`, any entered string is accepted, while `options` only power completion.
113113

114+
#### `include`
115+
116+
`include` lets you reuse a shared group of widgets from another file.
117+
118+
Example:
119+
120+
```yaml
121+
widgets:
122+
- stringParameter:
123+
id: screenName
124+
name: Screen name
125+
default: FeedScreen
126+
127+
- include:
128+
file: shared/codeowners.widgets.yaml
129+
```
130+
131+
Rules:
132+
133+
- `file` is resolved relative to the file where the include is declared;
134+
- the included file should contain only the top-level `widgets` section;
135+
- includes are expanded in place, so widget order is preserved;
136+
- nested includes are supported;
137+
- circular includes are forbidden;
138+
- widget ids should remain unique after include expansion.
139+
140+
If an included widget uses `suggestParameter.options.source`, that CSV path is also resolved
141+
relative to the included file, not to the root recipe.
142+
114143
---
115144

116145
[Back to `recipe.yaml` content](../RECIPE_CONTENT.md)

docs/ru/RECIPE_CONTENT.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ widgets:
8787
options:
8888
source: options/modules.csv
8989

90+
- include:
91+
file: shared/codeowners.widgets.yaml
92+
9093
- stringParameter:
9194
id: moduleName
9295
name: Fragment Toothpick Module
@@ -191,4 +194,4 @@ recipe:
191194

192195
---
193196

194-
[Обратно к содержанию](../../plugins/hh-geminio/README.md#Содержание)
197+
[Обратно к содержанию](../../README.md#Содержание)

docs/ru/recipe_content/WIDGETS.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,35 @@ CSV-файлы читаются как UTF-8 и поддерживают стр
113113
возьмёт `default`, а если и `default` отсутствует или невалиден - откатится к первому option
114114
- при `sealed: false` можно ввести любую строку, а `options` используются только для completion.
115115

116+
#### `include`
117+
118+
`include` позволяет переиспользовать общую группу виджетов из другого файла.
119+
120+
Пример:
121+
122+
```yaml
123+
widgets:
124+
- stringParameter:
125+
id: screenName
126+
name: Screen name
127+
default: FeedScreen
128+
129+
- include:
130+
file: shared/codeowners.widgets.yaml
131+
```
132+
133+
Правила:
134+
135+
- `file` вычисляется относительно файла, в котором объявлен include
136+
- включаемый файл должен содержать только top-level секцию `widgets`
137+
- include раскрывается на месте, поэтому порядок виджетов сохраняется
138+
- nested include поддерживаются
139+
- циклические include запрещены
140+
- после раскрытия include все `id` виджетов должны оставаться уникальными
141+
142+
Если включённый виджет использует `suggestParameter.options.source`, путь до CSV тоже вычисляется
143+
относительно включённого файла, а не корневого recipe.
144+
116145
---
117146

118147
[Обратно к устройству "рецептов"](../RECIPE_CONTENT.md)

src/main/kotlin/ru/hh/plugins/geminio/sdk/recipe/parsers/GeminioRecipeParser.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ internal fun String.parseGeminioRecipeFromYamlFile(): GeminioRecipe {
2525
freemarkerTemplatesRootDirPath = recipeFile.parent,
2626
requiredParams = configMap.toRequiredParams(),
2727
optionalParams = configMap.toOptionalParams(),
28-
widgetsSection = configMap.toWidgetsSection(recipeRootDirPath = recipeFile.parent),
28+
widgetsSection = configMap.toWidgetsSection(recipeFilePath = recipeFile.path),
2929
predefinedFeaturesSection = configMap.toPredefinedFeaturesSection(),
3030
globalsSection = configMap.toGlobalsSection(),
3131
recipeCommands = configMap.toRecipeCommandsSection()
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package ru.hh.plugins.geminio.sdk.recipe.parsers.widgets
2+
3+
internal data class RawWidgetDefinition(
4+
val definition: Map<String, Any>,
5+
val sourceFilePath: String?,
6+
)
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
@file:Suppress("UNCHECKED_CAST")
2+
3+
package ru.hh.plugins.geminio.sdk.recipe.parsers.widgets
4+
5+
import ru.hh.plugins.geminio.sdk.recipe.parsers.ParsersErrorsFactory.rootSectionErrorMessage
6+
import ru.hh.plugins.geminio.sdk.recipe.parsers.ParsersErrorsFactory.sectionErrorMessage
7+
import ru.hh.plugins.geminio.sdk.recipe.parsers.ParsersErrorsFactory.sectionRequiredParameterErrorMessage
8+
import ru.hh.plugins.utils.yaml.YamlUtils
9+
import java.io.File
10+
11+
internal fun Map<String, Any>.resolveRawWidgetDefinitions(
12+
recipeFilePath: String? = null,
13+
): List<RawWidgetDefinition> {
14+
val normalizedRecipeFilePath = recipeFilePath?.let(::normalizePath)
15+
16+
return resolveRawWidgetDefinitions(
17+
sourceFilePath = normalizedRecipeFilePath,
18+
includeChain = normalizedRecipeFilePath?.let(::listOf).orEmpty(),
19+
)
20+
}
21+
22+
private fun Map<String, Any>.resolveRawWidgetDefinitions(
23+
sourceFilePath: String?,
24+
includeChain: List<String>,
25+
): List<RawWidgetDefinition> {
26+
val widgetsList = requireNotNull(this[KEY_WIDGETS_SECTION] as? List<*>) {
27+
rootSectionErrorMessage(KEY_WIDGETS_SECTION)
28+
}
29+
30+
return widgetsList.flatMap { rawWidget ->
31+
val widgetMap = requireNotNull(rawWidget as? Map<String, Any>) {
32+
sectionErrorMessage(
33+
KEY_WIDGETS_SECTION,
34+
"Every widget entry should be an object.",
35+
)
36+
}
37+
38+
val includeMap = widgetMap[KEY_INCLUDE_PARAMETER_TYPE] as? Map<String, Any>
39+
if (includeMap == null) {
40+
listOf(
41+
RawWidgetDefinition(
42+
definition = widgetMap,
43+
sourceFilePath = sourceFilePath,
44+
)
45+
)
46+
} else {
47+
require(widgetMap.keys == setOf(KEY_INCLUDE_PARAMETER_TYPE)) {
48+
sectionErrorMessage(
49+
KEY_WIDGETS_SECTION,
50+
"Include widget entry should declare only '$KEY_INCLUDE_PARAMETER_TYPE' " +
51+
"[keys: ${widgetMap.keys}].",
52+
)
53+
}
54+
includeMap.resolveIncludedWidgetDefinitions(
55+
parentSourceFilePath = sourceFilePath,
56+
includeChain = includeChain,
57+
)
58+
}
59+
}
60+
}
61+
62+
private fun Map<String, Any>.resolveIncludedWidgetDefinitions(
63+
parentSourceFilePath: String?,
64+
includeChain: List<String>,
65+
): List<RawWidgetDefinition> {
66+
val sectionName = "$KEY_WIDGETS_SECTION:$KEY_INCLUDE_PARAMETER_TYPE"
67+
val includeFilePath = requireNotNull(this[KEY_INCLUDE_FILE] as? String) {
68+
sectionRequiredParameterErrorMessage(
69+
sectionName = sectionName,
70+
key = KEY_INCLUDE_FILE,
71+
)
72+
}
73+
require(this.keys == setOf(KEY_INCLUDE_FILE)) {
74+
sectionErrorMessage(
75+
sectionName,
76+
"Include entry should declare only '$KEY_INCLUDE_FILE'.",
77+
)
78+
}
79+
80+
val parentFile = requireNotNull(parentSourceFilePath) {
81+
sectionErrorMessage(
82+
sectionName,
83+
"Include resolution requires source file context.",
84+
)
85+
}
86+
val resolvedIncludeFile = File(File(parentFile).parentFile, includeFilePath)
87+
val normalizedIncludeFilePath = normalizePath(resolvedIncludeFile.path)
88+
89+
require(normalizedIncludeFilePath !in includeChain) {
90+
val includeChainText = (includeChain + normalizedIncludeFilePath).joinToString(" -> ")
91+
sectionErrorMessage(
92+
sectionName,
93+
"Circular widgets include detected [chain: $includeChainText].",
94+
)
95+
}
96+
97+
val includedConfigMap = YamlUtils.loadFromConfigFile(normalizedIncludeFilePath) { throwable ->
98+
throwable.printStackTrace()
99+
} ?: throw IllegalArgumentException(
100+
sectionErrorMessage(
101+
sectionName,
102+
"Cannot load include file [path: $normalizedIncludeFilePath].",
103+
)
104+
)
105+
106+
require(includedConfigMap.keys == setOf(KEY_WIDGETS_SECTION)) {
107+
sectionErrorMessage(
108+
sectionName,
109+
"Included widgets file should contain only '$KEY_WIDGETS_SECTION' top-level section " +
110+
"[path: $normalizedIncludeFilePath, keys: ${includedConfigMap.keys}].",
111+
)
112+
}
113+
114+
return includedConfigMap.resolveRawWidgetDefinitions(
115+
sourceFilePath = normalizedIncludeFilePath,
116+
includeChain = includeChain + normalizedIncludeFilePath,
117+
)
118+
}
119+
120+
private fun normalizePath(path: String): String {
121+
return File(path).canonicalFile.path
122+
}
123+
124+
private const val KEY_WIDGETS_SECTION = "widgets"
125+
private const val KEY_INCLUDE_PARAMETER_TYPE = "include"
126+
private const val KEY_INCLUDE_FILE = "file"

src/main/kotlin/ru/hh/plugins/geminio/sdk/recipe/parsers/widgets/WidgetsSectionParser.kt

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ package ru.hh.plugins.geminio.sdk.recipe.parsers.widgets
44

55
import ru.hh.plugins.geminio.sdk.recipe.models.widgets.RecipeParameter
66
import ru.hh.plugins.geminio.sdk.recipe.models.widgets.WidgetsSection
7-
import ru.hh.plugins.geminio.sdk.recipe.parsers.ParsersErrorsFactory.rootSectionErrorMessage
7+
import ru.hh.plugins.geminio.sdk.recipe.parsers.ParsersErrorsFactory.sectionErrorMessage
88
import ru.hh.plugins.geminio.sdk.recipe.parsers.ParsersErrorsFactory.sectionUnknownEnumKeyErrorMessage
9+
import java.io.File
910

1011
private const val KEY_WIDGETS_SECTION = "widgets"
1112

@@ -17,25 +18,27 @@ private const val KEY_SUGGEST_PARAMETER_TYPE = "suggestParameter"
1718
* Parser from YAML to [ru.hh.plugins.geminio.sdk.recipe.models.widgets.WidgetsSection].
1819
*/
1920
internal fun Map<String, Any>.toWidgetsSection(
20-
recipeRootDirPath: String? = null,
21+
recipeFilePath: String? = null,
2122
): WidgetsSection {
22-
val widgetsList = requireNotNull(this[KEY_WIDGETS_SECTION] as? List<Map<String, Any>>) {
23-
rootSectionErrorMessage(KEY_WIDGETS_SECTION)
23+
val rawWidgetDefinitions = resolveRawWidgetDefinitions(recipeFilePath)
24+
val parameters = rawWidgetDefinitions.map { rawWidgetDefinition ->
25+
rawWidgetDefinition.definition.toRecipeParameter(rawWidgetDefinition.sourceFilePath)
2426
}
2527

28+
validateUniqueWidgetIds(parameters, rawWidgetDefinitions)
29+
2630
return WidgetsSection(
27-
parameters = widgetsList.map { widget ->
28-
widget.toRecipeParameter(recipeRootDirPath)
29-
}
31+
parameters = parameters
3032
)
3133
}
3234

3335
private fun Map<String, Any>.toRecipeParameter(
34-
recipeRootDirPath: String?,
36+
sourceFilePath: String?,
3537
): RecipeParameter {
3638
val stringParameterMap = this[KEY_STRING_PARAMETER_TYPE] as? Map<String, Any>
3739
val booleanParameterMap = this[KEY_BOOLEAN_PARAMETER_TYPE] as? Map<String, Any>
3840
val suggestParameterMap = this[KEY_SUGGEST_PARAMETER_TYPE] as? Map<String, Any>
41+
val sourceDirPath = sourceFilePath?.let { filePath -> File(filePath).parent }
3942

4043
return when {
4144
stringParameterMap != null -> {
@@ -49,7 +52,7 @@ private fun Map<String, Any>.toRecipeParameter(
4952
suggestParameterMap != null -> {
5053
suggestParameterMap.toWidgetsSuggestParameter(
5154
sectionName = "$KEY_WIDGETS_SECTION:$KEY_SUGGEST_PARAMETER_TYPE",
52-
recipeRootDirPath = recipeRootDirPath,
55+
recipeRootDirPath = sourceDirPath,
5356
)
5457
}
5558

@@ -68,3 +71,26 @@ private fun Map<String, Any>.toRecipeParameter(
6871
}
6972
}
7073
}
74+
75+
private fun validateUniqueWidgetIds(
76+
parameters: List<RecipeParameter>,
77+
rawWidgetDefinitions: List<RawWidgetDefinition>,
78+
) {
79+
val duplicatedIds = parameters
80+
.zip(rawWidgetDefinitions)
81+
.groupBy(
82+
keySelector = { (parameter, _) -> parameter.id },
83+
valueTransform = { (_, rawDefinition) -> rawDefinition.sourceFilePath ?: "<inline>" },
84+
)
85+
.filterValues { sourceFiles -> sourceFiles.size > 1 }
86+
87+
require(duplicatedIds.isEmpty()) {
88+
val duplicateDescriptions = duplicatedIds.entries.joinToString { (parameterId, sourceFiles) ->
89+
"id='$parameterId', files=${sourceFiles.distinct()}"
90+
}
91+
sectionErrorMessage(
92+
KEY_WIDGETS_SECTION,
93+
"Widget parameter ids should be unique [$duplicateDescriptions].",
94+
)
95+
}
96+
}

0 commit comments

Comments
 (0)