Часть 1
Часть 2

После публикации второй статьи, я продолжил изучать библиотеки и развивать свою хотелку с циклом. В итоге начал с варианта добавления атрибута 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
	}

согласно описанному выше алгоритму.

  1. Получаем схему блока 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.

  1. Проходимся по найденным блокам 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 {
          ...
		}
	}
  1. Если атрибут не найден (попадаем в 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)
		}
  1. Если атрибут найден.

Вычисляем его как выражение.

...
		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
}

Комментарии (0)