После публикации второй статьи, я продолжил изучать библиотеки и развивать свою хотелку с циклом. В итоге начал с варианта добавления атрибута 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 }
ссылка на оригинал статьи https://habr.com/ru/articles/697590/
Добавить комментарий