Usar um Hook Declarativo

Nesta seção, você encontra detalhes de como usar um Hook Declarativo.

O que são Hooks Declarativos?

Os Hooks Declarativos são uma forma de definir ações a serem executadas em momentos específicos da aplicação de um Template ou Plugin.

A aplicação de um Template ou Plugin possui duas fases distintas:

Fase 1. Perguntar ao usuário os parâmetros de entrada.
Fase 2. Interpolar os templates Jinja usando os valores informados para geração de arquivos.

Você pode executar as ações definidas nos Hooks Declarativos:

  • Antes de inserir os parâmetros de entrada (before-input)
  • Antes de interpolar os templates Jinja para a geração dos arquivos (before-render)
  • Depois de gerar os arquivos (after-render)

Como definir os Hooks Declarativos?

A definição dos Hooks Declarativos de um Template ou Plugin podem ser feitas no arquivo de configuração template.yaml e plugin.yaml , conforme o exemplo abaixo:

hooks:
  - type: run
    trigger: before-input
    working-dir: "{{project_name}}"
    commands:
      - echo plugin-python before-input!
  - type: run-script
    trigger: before-render
    script: script.py
  - type: edit
    trigger: after-render
    path: src/some-file.txt
    changes:
      - insert:
          line: 0
          value: "# Text inserted in first line\n\n"
          when:
            not-exists: "# Text inserted in first line"
      - search:
          string: "# Text inserted in first line"
          insert-before:
            snippet: snippets/insert-before-first-line.txt
          when:
            not-exists: "# Text inserted before first line"
      - search:
          pattern: (Text )inserted in( first line)
          replace-by:
            value: \1moved from\2
          when:
            not-exists: print(f"Hello {name}!")
  - type: render-templates
    trigger: after-render
    path: templates-java
    condition:
      variable: language
      operator: ==
      value: java

Gatilhos

Os gatilhos dos Hooks Declarativos são definidos pelo atributo trigger e indicam em que momento o Hook Declarativo irá executar, podendo assumir um dos seguintes valores:

  • before-input: Antes de perguntar os parâmetros para o usuário.
  • before-render: Antes da geração dos arquivos.
  • after-render: Após a geração dos arquivos.

Condições

A execução de um Hook Declarativo pode ser condicional e é controlada pelo atributo opcional condition. Você deve definir uma condição com:

1. Uma variável (variable)

2. Um operador (operator)

3. Em seguida, o valor de referência (value).

As variáveis acessíveis são as definidas pelos inputs e computed-inputs do Template/Plugin. No trecho de exemplo apresentado abaixo, é verificado se o valor do input language é igual à string java. Em caso positivo, são gerados os arquivos definidos pelos Templates que estão na pasta templates-java.

- type: render-templates
    trigger: after-render
    path: templates-java
    condition:
      variable: language
      operator: ==
      value: java

Os operadores disponíveis para condições são:

  • ==: Valida se os valores são iguais.
  • !=: Valida se os valores são diferentes.
  • >: Valida se a variável é maior ao valor.
  • <: Valida se a variável é menor ao valor.
  • >=: Valida se a variável é maior ou igual ao valor.
  • <=: Valida se a variável é menor ou igual ao valor.
  • containsAny: Valida se a variável do tipo lista contém qualquer um dos valores contidos em value.
  • containsAll: Valida se a variável do tipo lista contém todos os valores valores contidos em value.
  • containsOnly: Valida se a variável do tipo lista contém todos os valores valores contidos em value e não contém outros elementos.

Tipos de hooks declarativos

Existem 4 tipos de hooks declarativos que são definidos pelo atributo type:

  • run: Executa um ou mais comandos.
  • run-script: Executa um script Python que exporta uma função run.
  • edit: Faz edições em um arquivo existente.
  • edit-xml: Faz edições em um arquivo XML existente.
  • edit-json: Faz edições em um arquivo XML existente.
  • edit-yaml: Faz edições em um arquivo XML existente.
  • render-templates: Faz a interpolação de Templates Jinja de forma condicional.

A estrutura de um Hook Declarativo pode ter diferentes atributos, conforme o tipo que foi definido. Veja os exemplos abaixo:

run

Os Hooks Declarativos do tipo run servem para executar comandos. Alguns exemplos de utilização interessante são:

  • Instalar dependências no projeto;
  • Executar comandos de alguma ferramenta que faz parte da Stack;
  • Executar configurações através de scripts shell ou batch.

Veja abaixo um exemplo simples de um Hook Declarativo do tipo run:

    - type: run
    trigger: before-input
    commands:
      - echo hello from before-input!
      - echo you can define multiple commands in a run hook!

Ao aplicar o Template/Plugin que define esse Hook Declarativo, antes de perguntar os parâmetros de entrada, o console mostra as seguintes frases:

  • hello from before-input!
  • you can define multiple commands in a run hook!.

É possível utilizar conditions em um Hook, conforme no exemplo abaixo:

name: my-plugin
description: Describe your plugin explaining its purpose
types:
  - app
inputs:
  - label: Deseja instalar as dependências?
    type: bool
    name: install_dependencies
hooks:
  - type: run
    trigger: after-render
    commands: 
      - echo Instalando dependências
      - npm install
    condition:
      variable: install_dependencies
      operator: ==
      value: true

Neste exemplo o input de tipo booleano faz uma pergunta a pessoa usuária para instalar dependências. A condition faz com que neste caso o Hook seja executado apenas caso a resposta seja verdadeira ( operador == e value true), somente então os comandos de npm install serão executados para instalar as dependências.

É possível definir comandos diferentes por sistema operacional. Veja o exemplo abaixo:

    - type: run
    trigger: before-input
    working-dir: "{{project_name}}/some/dir"
    linux:
      - echo hello from before-input on linux!
    windows:
      - cmd /C echo hello from before-input on windows!
    mac:
      - echo hello from before-input on mac!

Neste caso, ao aplicar o Template/Plugin, a mensagem que aparece no console será diferente para cada sistema operacional. Os sistemas operacionais suportados são:

  • linux
  • mac
  • windows

O atributo working-dir permite definir um diretório, relativo à raiz do projeto, onde os comandos serão executados. Caso o atributo não seja informado, os comandos serão executados na raiz do projeto. Na definição do working-dir podem ser usadas expressões Jinja.

Comando run no Windows

Para executar outras aplicações através do run no Windows, utilize um dos exemplos abaixo:

  1. Usando o cmd. Nesse caso, abre uma janela do CMD extra quando o plugin está sendo aplicado, mas depois é fechada automaticamente.
     - type: run
    trigger: after-render
    working-dir: "{{project_name}}/some/dir"
    linux:
      - npm install
    windows:
      - cmd /c npm install
    mac:
      - npm instal
  1. Chame o binário do npm diretamente, aqui o npm é executado na mesma janela:
    - type: run
    trigger: after-render
    working-dir: "{{project_name}}/some/dir"
    linux:
      - npm install
    windows:
      - npm.cmd install
    mac:
      - npm instal

run-script

Os Hooks Declarativos do tipo run-script servem para executar scripts Python, dentro de um arquivo plugin.yaml ou template.taml. Veja abaixo um exemplo de um Hook Declarativo do tipo run-script:

  - type: run-script
    trigger: before-render
    script: path/to/script.py

O atributo script define o caminho onde o script a ser executado está na estrutura de arquivos do Template/Plugin.

É comum esperar que neste script, uma função com o nome run esteja definida. Essa função recebe como parâmetro um objeto da classe Metadata (class Metadata) do template framework e que devolve em seu retorno um Metadata, podendo ser o mesmo recebido ou outro criado pela função.

Metadata

Quando o run-script for executado e exportado usando uma classe Metadata, você pode visualizar uma estrutura como a do exemplo abaixo:

import shutil
import os
from templateframework.metadata import Metadata

def run(metadata: Metadata = None):
    inputs = metadata.all_inputs()
    inputs_local = metadata.inputs
    inputs_global = metadata.global_inputs
    inputs_computed_global = metadata.global_computed_inputs
    target_path = metadata.target_path
    component_path = metadata.component_path
    stack_path = metadata.stack_path

Confira a descrição de cada item da classe:

  • inputs = metadata.all_inputs(): Retorna todos os inputs processados do Plugin/Template;
  • inputs_local = metadata.inputs: É o dicionário dos inputs;
  • inputs_global = metadata.global_inputs: Dicionário de global inputs;
  • inputs_computed_global = metadata.global_computed_inputs: Dicionário de global conputed inputs;
  • target_path = metadata.target_path: É o path onde o Plugin/Template está sendo executado.
  • component_path = metadata.component_path: É o path do Plugin/Template (~/.stk/stacks//).
  • stack_path = metadata.stack_path: É o path da stack (~/.stk/stacks/).

Veja abaixo um exemplo simples de definição de um script, que pode ser executado como um hook declarativo:

from templateframework.metadata import Metadata
def run(metadata: Metadata = None):
    print("Hello from script.py!")
    return metadata

No script você pode usar a biblioteca padrão do Python 3.8 e as bibliotecas extra que são dependências do STK CLI. Veja abaixo, algumas que são incluídas:

edit

Um Hook Declarativo do tipo edit define alterações feitas em arquivos existentes. Veja abaixo um exemplo de definição de um Hook Declarativo do tipo edit:

    - type: edit
    trigger: after-render
    path: src/some-file.txt
    changes:
      - insert:
          line: 0
          value: "# Text inserted in first line\n\n"
          when:
            not-exists: "# Text inserted in first line"
      - search:
          string: "# Text inserted in first line"
          insert-before:
            snippet: snippets/insert-before-first-line.txt
          when:
            not-exists: "# Text inserted before first line"
      - search:
          pattern: (Text )inserted in( first line)
          replace-by:
            value: \1moved from\2
          when:
            not-exists: print(f"Hello {name}!")
  • O atributo path define o arquivo a ser editado pelo Hook Declarativo. Você pode usar expressões Jinja em sua definição.

  • O atributo changes é uma lista de ações de edição que serão executadas na ordem em que aparecem. As ações de edição podem ser insert ou search.

Ações de edição do tipo insert possuem a estrutura abaixo:

      - insert:
          line: 0
          value: "# Text inserted in first line\n\n"
          when:
            not-exists: "# Text inserted in first line"
  • O atributo line define em qual linha será feita a inserção. A primeira linha é 0, é possível usar o valor -1 para inserir na última linha.

  • O atributo value indica qual será o texto inserido na linha.

  • O atributo when indica as condições em que, quando avaliadas como verdadeiras, a edição é aplicada. Existem duas condições disponíveis: not-exists e not-exists-snippet. Caso não sejam satisfeitas, a ação de edição não será executada.

A condição not-exists define que a inserção será realizada apenas se o texto definido na condição não for encontrado no arquivo sendo editado.

A condição not-exists-snippet possui um comportamento semelhante ao not-exists, mas usa o conteúdo de um arquivo de snippet como base para verificar a condição.

Apesar de não ser comum, é possível usar not-exists e not-exists-snippet em conjunto, a avaliação neste caso é o resultado de um and entre os dois resultados.

É possível substituir o atributo value por snippet para definir um arquivo com o conteúdo a ser inserido. Veja o exemplo abaixo:

      - insert:
          line: 0
          snippet: path/to/snippet.txt
          when:
            not-exists: "# Text inserted in first line"

No exemplo acima, o texto foi inserido na primeira linha do arquivo e o conteúdo do arquivo path/to/snippet.txt, que está na estrutura do Plugin/Template, foi editado.

Ações de edição do tipo search possuem a estrutura abaixo:

      - search:
          string: "# Text inserted in first line"
          ou
          pattern: (some)\s+(regular)\s+(expression)
          ou
          snippet: snippets/snippet.txt
          replace-by:
            snippet: snippets/snippet.txt
            ou
            value: \1 \2 \3
          ou
          insert-before:
            snippet: snippets/snippet.txt
            ou
            value: a string
          ou
          insert-after:
            snippet: snippets/snippet.txt
            ou
            value: a string
          when:
            not-exists: "# Text inserted before first line"

Os atributos string, pattern ou snippet indicam o que será buscado no arquivo sendo editado.

Caso seja utilizado string a busca é via match exato da string informada, caso seja pattern pode-se definir uma expressão regular para realizar a busca e caso seja snippet pode-se definir o caminho para um arquivo cujo conteúdo será usado como parâmetro para realizar a busca.

A ação de edição busca pela primeira ocorrência do texto no arquivo, e aplica uma das seguintes operações: replace-by, insert-before ou insert-after.

  • A operação replace-by substitui o primeiro match encontrado no arquivo sendo editado pelo texto definido em value ou no snippet, que não devem ser usados em conjunto.

  • A operação insert-before insere o value ou snippet na linha anterior à linha onde o primeiro match foi encontrado.

  • A operação insert-after insere o value ou snippet na próxima linha em relação à linha onde o primeiro match foi encontrado.

Caso seja usado pattern os grupos podem ser usados no replace-by, veja o exemplo abaixo:

      - search:
         pattern: (some)\s+(regular)\s+(expression)
         replace-by:
           value: \1 \2 \3

O conteúdo tanto de value quanto do arquivo indicado em snippet pode conter expressões Jinja que serão interpoladas antes da aplicação.

edit-xml

Um Hook Declarativo do tipo edit-xml define alterações feitas em arquivos XML existentes. Confira abaixo um exemplo de definição de um Hook Declarativo do tipo edit-xml:

hooks:
  - type: edit-xml
    trigger: after-render
    path: pom.xml
    encoding: UTF-8
    indent: "\t"
    changes:
      - xpath: .//dependency
        append:
          value: |
            <config>
              <vsync>1</vsync>
            </config>
        prepend:
          value: <{{input_tag}}>{{input_body}}</{{input_tag}}>

      - xpath: .//modelVersion
        text:
          value: 10.0.1

      - xpath: .//description
        text:
          snippet: snippets/description.txt

      - xpath: .//dependencies
        remove-attributes:
          - name: css

      - xpath: .//project.build.sourceEncoding
        attributes:
          - name: btn-name
            value: Build UTF-8

      - xpath: .//comments
        clear: true
  • O atributo path define o arquivo a ser editado pelo Hook Declarativo. Você pode usar expressões Jinja em sua definição.

  • O atributo encoding define o tipo de encoding que será gravado no arquivo, e aceita os formatos comuns ao XML tal como [utf-8, ascii, windows-1252], caso não seja informado, será mantido o padrão original do arquivo. O seu uso é Opcional.

  • O atributo indent define o(s) caracterer(s) para a indentação do documento gerado, o valor padrão é \t. O seu uso é Opcional.

  • O atributo changes é uma lista de ações de edição que serão executadas na ordem em que aparecem. E o seu bloco tem como obrigatório o parâmetro xpath.
  changes:
    - xpath: /people/person[last()]
      ...
  • O atributo xpath, permite inserir a linguagem de busca de XML. O seu uso é Obrigatório.

Os atributos a seguir são opcionais, porém para resultado adequado é necessário implemetar ao menos uma opção, em caso de múltiplos atributos, eles serão aplicados na seguinte ordem:

  1. clear
  2. remove-attributes
  3. append
  4. prepend
  5. next
  6. previous
  7. text
  8. attributes
  • O atributo append, adiciona nós XML dentro do contexto do resultado do xpath no fim da lista. Os campos disponíveis são value ou snippet.

    • O campo value recebe o valor para ser adicionado ao XML.
    • O campo snippet recebe o snippet para ser adicionado ao XML.
  • O atributo prepend, adiciona nós XML dentro do contexto do resultado do xpath no início da lista. Os campos disponíveis são value ou snippet.

    • O campo value recebe o valor para ser adicionado ao XML.
    • O campo snippet recebe o snippet para ser adicionado ao XML.
  • O atributo next, adiciona nós XML no mesmo nível de contexto do resultado do xpath no fim da lista. Os campos disponíveis são value ou snippet.

    • O campo value recebe o valor para ser adicionado ao XML.
    • O campo snippet recebe o snippet para ser adicionado ao XML.
  • O atributo previous, adiciona nós XML no mesmo nível de contexto do resultado do xpath no início da lista. Os campos disponíveis são value ou snippet.

    • O campo value recebe o valor para ser adicionado ao XML.
    • O campo snippet recebe o snippet para ser adicionado ao XML.
  - xpath: //ns:dependency
    append:
      value: |
        <config>
          <vsync>1</vsync>
        </config>
    prepend:
      value: <{{input_tag}}>{{input_body}}</{{input_tag}}>
    next:
      value: <version>1.0.0</version>
    previous:
      snippet: snippets/fragment.xml
  • O atributo text, adiciona texto dentro do contexto do resultado do xpath. Os campos disponíveis são value ou snippet.

    • O campo value recebe o valor para ser adicionado ao XML.
    • O campo snippet recebe o snippet para ser adicionado ao XML.
  • O atributo attributes, adiciona atributos e valores a um nó. Os campos disponíveis são name e value.

    • O campo name recebe o valor para ser adicionado ao XML.
    • O campo value recebe o snippet para ser adicionado ao XML.
  - xpath: //ns:button
    attributes:
      - name: btn-name
        value: Build UTF-8
      - name: style
        value: "color: #007"
  • O atributo remove-attributes, remove os atributos de um nó. Os campos disponíveis são um ou mais name.
  - xpath: //ns:dependency
    remove-attributes:
      - name: css
      - name: style
      - name: data
  • O atributo clear, remove todo o conteúdo do nó dado o contexto do xpath.
  - xpath: //ns:dependency
    clear: true

edit-json

Um Hook Declarativo do tipo edit-json define alterações feitas em arquivos JSON existentes. Confira abaixo um exemplo de definição de um Hook Declarativo do tipo edit-json:

name: "hook-edit-json"
description: Test edit-json in hooks
types:
  - app
inputs:
  - label: Just a text
    type: text
    name: json_body
  - label: Just a Tag
    type: text
    name: json_tag
hooks:
  - type: edit-json
    trigger: after-render
    path: package.json
    indent: "\t"
    encoding: utf-8
    changes:
      - jsonpath: "$.scripts"
        update:
          value: |
            {
            "test": "ola 123",
            "ola": "como vai você"
            }
      - jsonpath: "$"
        remove:
          - name: private
          - name: devDependencies
      - jsonpath: "$.log"
        clear: true
  • O atributo path define o arquivo a ser editado pelo Hook Declarativo. Você pode usar expressões Jinja em sua definição.

  • O atributo encoding define o tipo de encoding a ser gravado no arquivo, e aceita os formatos [ascii, utf-8, utf-16]. O valor padrão é o utf-8.

  • O atributo indent define o(s) caracterer(s) ou número de espaços para a indentação do documento gerado. O valor padrão é o 2.

  • O atributo changes é uma lista de ações de edição que serão executadas na ordem em que aparecem. E o seu bloco tem como obrigatório o parâmetro jsonpath.

  changes:
    - jsonpath: "$.packages.version"
      ...
  • O atributo jsonpath, permite inserir a linguagem de busca do JSON. O uso é Obrigatório.

Se necessário, consulte o Suporte do jsonpath

Os atributos a seguir são opcionais, porém para resultado adequado é necessário implemetar ao menos uma opção, em caso de múltiplos atributos, eles serão aplicados na seguinte ordem:

  1. clear
  2. remove
  3. update
  • O atributo clear, remove todo o conteúdo do nó JSON, dado o contexto do jsonpath. Os valores aceitos são true ou false.
  - jsonpath: "$.devDependencies"
    clear: true
  • O atributo remove, remove um elemento de dicionário. O campo disponível é o name.
    • O campo name recebe o nome do elemento a ser removido do dicionário JSON.
  - jsonpath: "$.scripts"
    remove:
      - name: prepare
  • O atributo update, altera ou acrescenta um item dentro de um resultado de um jsonpath. Os campos disponíveis são value ou snippet.
    • O campo value recebe o valor para ser atualizar o XML.
    • O campo snippet recebe o snippet para atualizar o XML.
      - jsonpath: "$.workspaces"
        update:
          value: |
            "qa"

edit-yaml

Um Hook Declarativo do tipo edit-yaml define alterações feitas em arquivos YAML existentes. Veja abaixo um exemplo de definição de um hook declarativo do tipo edit-yaml:

name: "hook-edit-yaml"
description: Test edit-yaml in hooks
types:
  - app
inputs:
  - label: Just a text
    type: text
    name: yaml_body
  - label: Just a Tag
    type: text
    name: yaml_tag
hooks:
  - type: edit-yaml
    trigger: after-render
    path: package.yaml
    indent: "\t"
    encoding: utf-8
    changes:
      - yamlpath: "$.scripts"
        update:
          value: |
            {
            "test": "ola 123",
            "ola": "como vai você"
            }
      - yamlpath: "$"
        remove:
          - name: private
          - name: devDependencies
      - yamlpath: "$.log"
        clear: true
  • O atributo path define o arquivo a ser editado pelo Hook Declarativo. Você pode usar expressões Jinja em sua definição.

  • O atributo encoding define o tipo de encoding a ser gravado o arquivo, e aceita os formatos [ascii, utf-8, utf-16]. O valor padrão é o utf-8.

  • O atributo indent define o(s) caracterer(s) ou número de espaços para a indentação do documento gerado, O valor padrão é o 2.

  • O atributo changes é uma lista de ações de edição que serão executadas na ordem em que aparecem. E o seu bloco tem como obrigatório o parâmetro jsonpath.

  changes:
    - yamlpath: "$.packages.version"
      ...
  • O atributo yamlpath, permite usar a linguagem de busca de JSON no arquivo YAML. O uso é Obrigatório.

Suporte do yamlpath “jsonpath”

Os atributos a seguir são opcionais, porém para resultado adequado é necessário implemetar ao menos uma opção, em caso de múltiplos atributos, eles serão aplicados na seguinte ordem:

  1. clear
  2. remove
  3. update
  • O atributo clear, remove todo o conteúdo do nó YAML, dado o contexto do yamlpath. Os valores aceitos são true ou false.
  - yamlpath: "$.devDependencies"
    clear: true
  • O atributo name, remove um elemento de dicionário. O campo disponível é o name.
    • O campo name recebe o nome do elemento a ser removido do dicionário YAML.
  - yamlpath: "$.scripts"
    remove:
      - name: prepare
  • O atributo update, altera ou acrescenta um item dentro de um resultado de um yamlpath. Os campos disponíveis são value ou snippet.
    • O campo value recebe o valor para ser atualizar o XML.
    • O campo snippet recebe o snippet para atualizar o XML.
      - yamlpath: "$.workspaces"
        update:
          value: |
            "qa"

render-templates

Um Hook Declarativo do tipo render-templates pode ser usado para executar a geração condicional de arquivos. Ao usar um Hook Declarativo desse tipo, associado a uma condição, é possível controlar se algum arquivo será ou não gerado de acordo com uma condição. Veja um exemplo de utilização:

- type: render-templates
  trigger: after-render
  path: templates-java
  condition:
    variable: language
    operator: ==
    value: java
- type: render-templates
  trigger: after-render
  path: templates-kotlin
  condition:
    variable: language
    operator: ==
    value: kotlin

No caso acima, quando o input language for preenchido com o valor java, ele gerará os arquivos que estão na pasta templates-java. Caso o input esteja preenchido com o valor kotlin, ele gerará os arquivos que estão na pasta templates-kotlin

Confira também