После публикации второй статьи, я продолжил изучать библиотеки и развивать свою хотелку с циклом. В итоге начал с варианта добавления атрибута for_each в блок create.
К моему удивлению, рабочее решение родилось в течение часа ????. И для его реализации пришлось обратиться к PartialContent и BodySchema из "Способа 4".
Изучая внутренности метода gohcl.DecodeBody(), я наткнулся на метод получения BodySchema из используемой Go структуры. Для этого используется метод gohcl.ImpliedBodySchema(), который вход принимает interface{}, а на выходе дает hcl.BodySchema.
Таким образом, можно продолжать работать со структурой блока create без создания новой с типом BodySchema.
Итак, актуальный вариант блока create выглядит так:
variables {
developers = ["jira_user_2", "jira_user_3", "jira_user_4"]
tester = "jira_user_1"
team_lead = "jira_user_5"
tech_lead = "jira_user_5"
release_engineer = "jira_user_6"
services = [
{ name = "service_A", skip = false },
{ name = "service_B", skip = true },
{ name = "service_C", skip = false },
]
}
create "Task" {
project = "AG" # required
# required
summary = "${iter.name} // Обновить библиотеку Library_A до актуальной версии"
# optional
description = <<DESC
Нужно обновить библиотеку Library_A до актуальной версии.
После обновления проверить сервис на regress
DESC
app_layer = "Backend" # optional
components = ["${iter.name}"] # optional
sprint = 100 # optional
epic = "AG-6815" # optional
labels = ["need-regress"] # optional
story_point = 2 # optional
qa_story_point = 1 # optional
assignee = developers.0 # optional
developer = developers.0 # optional
team_lead = team_lead # optional
tech_lead = tech_lead # optional
release_engineer = release_engineer # optional
tester = tester # optional
for_each = [for service in services : service if !service.skip]
}
По моей задумке for_each должен иметь итерируемый тип. В процессе итераций в переменных будет доступна переменная с именем iter, в которую будет помещаться текущий элемент итерации.
В моем случае в for_each будет список сервисов, для которых нужно создать задачи. А так как не все сервисы нужно обновлять, то хотелось бы иметь возможность их фильтровать, для этого в структуру добавлен атрибут skip.
В данном примере мы должны получить 2 issue в Jira, где в заголовке и компонентах будут упоминаться service_A и service_C.
&main.CreateBlocks{
Create: {
{
Type: "Task",
Project: "AG",
Summary: "service_A // Обновить библиотеку Library_A до актуальной версии",
Description: "Нужно обновить библиотеку Library_A до актуальной версии.\nПосле обновления проверить сервис на regress\n",
AppLayer: "Backend",
Components: {"service_A"},
SprintId: 100,
Epic: "AG-6815",
Labels: {"need-regress"},
StoryPoint: 2,
QaStoryPoint: 1,
Assignee: "jira_user_2",
Developer: "jira_user_2",
TeamLead: "jira_user_5",
TechLead: "jira_user_5",
ReleaseEngineer: "jira_user_6",
Tester: "jira_user_1",
Parent: "",
Remains: nil,
},
{
Type: "Task",
Project: "AG",
Summary: "service_C // Обновить библиотеку Library_A до актуальной версии",
Description: "Нужно обновить библиотеку Library_A до актуальной версии.\nПосле обновления проверить сервис на regress\n",
AppLayer: "Backend",
Components: {"service_C"},
SprintId: 100,
Epic: "AG-6815",
Labels: {"need-regress"},
StoryPoint: 2,
QaStoryPoint: 1,
Assignee: "jira_user_2",
Developer: "jira_user_2",
TeamLead: "jira_user_5",
TechLead: "jira_user_5",
ReleaseEngineer: "jira_user_6",
Tester: "jira_user_1",
Parent: "",
Remains: nil,
},
},
}
С конфигом разобрались. Перейдем к коду.
Так как for_each представляет из себя выражение, нам нужно заранее понимать, встречается ли этот атрибут в блоке create или нет, чтобы его заранее "посчитать" и еще наличие переменной iter в блоке тоже приведет к ошибке, так как парсер на момент вызова gohcl.DecodeBody() ничего об этой переменной не знает.
Поэтому задача сводится к следующим шагам:
- найти все блоки create
- проверить наличие for_each
- если атрибут найден, то "просчитать" его и запустить цикл, обрабатывая в каждой итерации текущий блок create и добавляя переменную iter
- если атрибута нет, то использовать привычную функцию gohcl.DecodeBody()
Первым шагом в добавляем в структуру create:
type createConfig struct {
...
Remains hcl.Body `hcl:",remain"`
}
и дорабатываем код
var createBlock CreateBlocks
diags = gohcl.DecodeBody(variablesBlock.Remains, ctx, &createBlock)
if diags.HasErrors() {
renderDiags(diags, parser.Files())
return nil, diags
}
согласно описанному выше алгоритму.
Получаем схему блока create и пытаемся их прочитать
var createBlocks CreateBlocks // p.s. мелкий рефакторинг
schema, _ := gohcl.ImpliedBodySchema(&createBlocks)
bc, _, diags := variablesBlock.Remains.PartialContent(schema)
if diags.HasErrors() {
renderDiags(diags, parser.Files())
return nil, diags
}
Так как перед этим мы прочитали блок variables, то в variablesBlock.Remains попадет оставшаяся часть Body, которую нам и нужно проанализировать. Для этого мы получаем схему для блока create и пытаемся прочитать ее через PartialContent в том оставшемся куске Body.
Найденные блоки create на выходе из функции вернуться в переменной bc с типом hcl.BodyContent.
Проходимся по найденным блокам create и проверяем наличие for_each
for _, block := range bc.Blocks {
var config createConfig
// получаем атрибуты в блоке
attr, diags := block.Body.JustAttributes()
if diags.HasErrors() {
renderDiags(diags, parser.Files())
return nil, diags
}
// проверяем, есть ли среди них for_each
forEach, found := attr["for_each"]
if found {
...
} else {
...
}
}
Если атрибут не найден (попадаем в else).
Вызываем DecodeBody, но в качестве Body передаем уже block.Body. Так как тип issue является лейблом (в структуре объявлен как label), то в block.Body он уже не попадает, так как относится к уровню выше. Поэтому нужно самостоятельно заполнить config.Type.
if found {
...
} else {
diags = gohcl.DecodeBody(block.Body, ctx, &config)
if diags.HasErrors() {
renderDiags(diags, parser.Files())
return nil, diags
}
config.Type = block.Labels[0]
// очищаем, чтобы не было мусора при выводе в консоль
config.Remains = nil
// добавляем очередной блок create в массив блоков
createBlocks.Create = append(createBlocks.Create, config)
}
Если атрибут найден.
Вычисляем его как выражение.
...
if found {
var forEachValue cty.Value
diags := gohcl.DecodeExpression(forEach.Expr, ctx, &forEachValue)
if diags.HasErrors() {
return nil, diags
}
...
Так как по моей задумке это итерируемый объект, то проходим в цикле по полученным значениям. Для этого у cty.Value есть метод ForEachElement, который на вход принимает callback функцию для обработки элемента.
// проходимся в цикле по полученным в for_each значениям
forEachValue.ForEachElement(func(key cty.Value, val cty.Value) (stop bool) {
// заполняем переменную iter текущим значением
// в моем случае это объект service с полями name и skip
ctx.Variables["iter"] = val
// передаем Body из блока на парсинг и подстановку переменных
diags = gohcl.DecodeBody(block.Body, ctx, &config)
if diags.HasErrors() {
renderDiags(diags, parser.Files())
return true
}
config.Type = block.Labels[0]
// очищаем, чтобы не было мусора при выводе в консоль
config.Remains = nil
// добавляем очередной блок create в массив блоков
createBlocks.Create = append(createBlocks.Create, config)
return false
})
Таким образом окончательные варианты структур выглядят так
type VariablesBlock struct {
Variables variables `hcl:"variables,block"`
Remains hcl.Body `hcl:",remain"`
}
type variables struct {
Remains hcl.Body `hcl:",remain"`
}
type CreateBlocks struct {
Create []createConfig `hcl:"create,block"`
}
type createConfig struct {
Type string `hcl:"type,label"`
Project string `hcl:"project"`
Summary string `hcl:"summary"`
Description string `hcl:"description,optional"`
AppLayer string `hcl:"app_layer,optional"`
Components []string `hcl:"components,optional"`
SprintId int `hcl:"sprint,optional"`
Epic string `hcl:"epic,optional"`
Labels []string `hcl:"labels,optional"`
StoryPoint int `hcl:"story_point,optional"`
QaStoryPoint int `hcl:"qa_story_point,optional"`
Assignee string `hcl:"assignee,optional"`
Developer string `hcl:"developer,optional"`
TeamLead string `hcl:"team_lead,optional"`
TechLead string `hcl:"tech_lead,optional"`
ReleaseEngineer string `hcl:"release_engineer,optional"`
Tester string `hcl:"tester,optional"`
Parent string `hcl:"parent,optional"`
Remains hcl.Body `hcl:",remain"`
}
И функция парсера так
func parse(filename string) (*CreateBlocks, error) {
parser := hclparse.NewParser()
f, diags := parser.ParseHCLFile(filename)
if diags.HasErrors() {
renderDiags(diags, parser.Files())
return nil, diags
}
ctx := &hcl.EvalContext{
Variables: map[string]cty.Value{},
Functions: map[string]function.Function{
"env": EnvFunc,
},
}
var variablesBlock VariablesBlock
_ = gohcl.DecodeBody(f.Body, ctx, &variablesBlock)
if variablesBlock.Variables.Remains != nil {
variables, diags := variablesBlock.Variables.Remains.JustAttributes()
if diags.HasErrors() {
renderDiags(diags, parser.Files())
return nil, diags
}
for key, variable := range variables {
var value cty.Value
diags := gohcl.DecodeExpression(variable.Expr, nil, &value)
if diags.HasErrors() {
return nil, diags
}
ctx.Variables[key] = value
}
}
var createBlocks CreateBlocks
schema, _ := gohcl.ImpliedBodySchema(&createBlocks)
bc, _, diags := variablesBlock.Remains.PartialContent(schema)
if diags.HasErrors() {
renderDiags(diags, parser.Files())
return nil, diags
}
for _, block := range bc.Blocks {
var config createConfig
attr, diags := block.Body.JustAttributes()
if diags.HasErrors() {
renderDiags(diags, parser.Files())
return nil, diags
}
forEach, found := attr["for_each"]
if found {
var forEachValue cty.Value
diags := gohcl.DecodeExpression(forEach.Expr, ctx, &forEachValue)
if diags.HasErrors() {
return nil, diags
}
forEachValue.ForEachElement(func(key cty.Value, val cty.Value) (stop bool) {
ctx.Variables["iter"] = val
diags = gohcl.DecodeBody(block.Body, ctx, &config)
if diags.HasErrors() {
renderDiags(diags, parser.Files())
return true
}
config.Type = block.Labels[0]
config.Remains = nil
createBlocks.Create = append(createBlocks.Create, config)
return false
})
delete(ctx.Variables, "iter")
} else {
diags = gohcl.DecodeBody(block.Body, ctx, &config)
if diags.HasErrors() {
renderDiags(diags, parser.Files())
return nil, diags
}
config.Type = block.Labels[0]
config.Remains = nil
createBlocks.Create = append(createBlocks.Create, config)
}
}
return &createBlocks, nil
}