como referenciar uma “configuração” do YAML de outro lugar no mesmo arquivo YAML?

145

Eu tenho o seguinte YAML:

paths:
  patha: /path/to/root/a
  pathb: /path/to/root/b
  pathc: /path/to/root/c

Como posso "normalizar" isso, removendo /path/to/root/dos três caminhos e defini-lo como sua própria configuração, algo como:

paths:
  root: /path/to/root/
  patha: *root* + a
  pathb: *root* + b
  pathc: *root* + c

Obviamente, isso é inválido, eu apenas inventei. Qual é a verdadeira sintaxe? Isso pode ser feito?

Andrew Bullock
fonte
1
Veja também: stackoverflow.com/a/41620747/42223
dreftymac 2/17/17

Respostas:

126

Eu não acho que isso é possível. Você pode reutilizar o "nó", mas não faz parte dele.

bill-to: &id001
    given  : Chris
    family : Dumars
ship-to: *id001

Isso é YAML e campos perfeitamente válidos givene familysão reutilizados em ship-tobloco. Você pode reutilizar um nó escalar da mesma maneira, mas não há como alterar o que está dentro e adicionar a última parte de um caminho a partir do YAML.

Se a repetição o incomoda tanto, sugiro que seu aplicativo fique ciente das rootpropriedades e adicione-o a todos os caminhos que parecem relativos, não absolutos.

vava
fonte
1
Ok, obrigado, sim, eu tenho que acrescentar o rootcódigo. nada demais.
Andrew Bullock
2
A resposta aceita não é precisa. Veja minha resposta para uma solução.
21420 Chris Johnson
como fazer isso, se o faturamento estiver em outro arquivo, que importamos para onde o destinatário está definido?
Prateek Jain #
@PrateekJain: se você estiver lidando com vários arquivos, provavelmente fará o melhor para avaliar uma biblioteca autônoma de aprimoramento de YAML, como a listada aqui. github.com/dreftymac/dynamic.yaml/blob/master/…
dreftymac
1
Veja o exemplo 2.9 em yaml.org/spec/1.2/spec.html ; pode-se também fazer referência escalares que é impressionante
akostadinov
72

Sim, usando tags personalizadas. Exemplo em Python, fazendo a !jointag unir cadeias de caracteres em uma matriz:

import yaml

## define custom tag handler
def join(loader, node):
    seq = loader.construct_sequence(node)
    return ''.join([str(i) for i in seq])

## register the tag handler
yaml.add_constructor('!join', join)

## using your sample data
yaml.load("""
paths:
    root: &BASE /path/to/root/
    patha: !join [*BASE, a]
    pathb: !join [*BASE, b]
    pathc: !join [*BASE, c]
""")

O que resulta em:

{
    'paths': {
        'patha': '/path/to/root/a',
        'pathb': '/path/to/root/b',
        'pathc': '/path/to/root/c',
        'root': '/path/to/root/'
     }
}

A matriz de argumentos para !joinpode ter qualquer número de elementos de qualquer tipo de dados, desde que eles possam ser convertidos em cadeia de caracteres, assim !join [*a, "/", *b, "/", *c]como o que você esperaria.

Chris Johnson
fonte
2
Gosto da sua solução, mais simples de codificar do que a minha, ao custo de YAML um pouco menos legível.
Anthon
7
Esta resposta merece mais votos positivos. É tecnicamente a resposta mais precisa, de acordo com a especificação YAML. Há uma ressalva, no entanto, de acordo com as implementações reais de YAML , existem poucas que realmente implementam a especificação completa de YAML. O pyyaml ​​do Python está acima e além de muitos outros em termos de uniformidade com a especificação.
dreftymac
5
A pergunta parece ser sobre como referenciar um valor em um arquivo yaml. Adicionar outra camada de código ao seu redor não seria minha solução preferida.
user2020056
1
@ ChrisJohnson Obrigado por esta resposta, eu queria saber se você tinha um documento de referência que listasse essa sintaxe. Eu já vi as especificações do YAML explicadas em vários lugares da Web, então só quero ter certeza de que estou olhando para a mesma referência que você. Obrigado!
user5359531
3
Esta solução não funcionou para mim ( python3?), No entanto, com uma simples modificação do acima, ela funciona conforme o esperado. Especificamente:yaml.SafeLoader.add_constructor(tag='!join', constructor=join) yaml.load(open(fpth, mode='r'), Loader=yaml.SafeLoader)
benjaminmgross
20

Outra maneira de ver isso é simplesmente usar outro campo.

paths:
  root_path: &root
     val: /path/to/root/
  patha: &a
    root_path: *root
    rel_path: a
  pathb: &b
    root_path: *root
    rel_path: b
  pathc: &c
    root_path: *root
    rel_path: c
Brian Bruggeman
fonte
5

Definição YML:

dir:
  default: /home/data/in/
  proj1: ${dir.default}p1
  proj2: ${dir.default}p2
  proj3: ${dir.default}p3 

Em algum lugar do thymeleaf

<p th:utext='${@environment.getProperty("dir.default")}' />
<p th:utext='${@environment.getProperty("dir.proj1")}' /> 

Saída: / home / data / in / / home / data / in / p1

Pavol
fonte
@AndrewBullock Acho que essa deve ser a resposta aceita, pois resolve exatamente o seu problema.
Honza Zidek
5
Não, não é um uso nativo da variável no YAML e não é especificado em nenhuma versão de especificação. Após algum teste, isso não funciona.
Arthur Lacoste
2
Isso provavelmente trabalhou para Pavol usando algo que pré-processado o yaml (ou seja, maven-recursos-plugin filtrar)
DeezCashews
1
Não é padrão Yaml
Dan Niero
3

Criei uma biblioteca, disponível no Packagist, que executa esta função: https://packagist.org/packages/grasmash/yaml-expander

Arquivo YAML de exemplo:

type: book
book:
  title: Dune
  author: Frank Herbert
  copyright: ${book.author} 1965
  protaganist: ${characters.0.name}
  media:
    - hardcover
characters:
  - name: Paul Atreides
    occupation: Kwisatz Haderach
    aliases:
      - Usul
      - Muad'Dib
      - The Preacher
  - name: Duncan Idaho
    occupation: Swordmaster
summary: ${book.title} by ${book.author}
product-name: ${${type}.title}

Exemplo de lógica:

// Parse a yaml string directly, expanding internal property references.
$yaml_string = file_get_contents("dune.yml");
$expanded = \Grasmash\YamlExpander\Expander::parse($yaml_string);
print_r($expanded);

Matriz resultante:

array (
  'type' => 'book',
  'book' => 
  array (
    'title' => 'Dune',
    'author' => 'Frank Herbert',
    'copyright' => 'Frank Herbert 1965',
    'protaganist' => 'Paul Atreides',
    'media' => 
    array (
      0 => 'hardcover',
    ),
  ),
  'characters' => 
  array (
    0 => 
    array (
      'name' => 'Paul Atreides',
      'occupation' => 'Kwisatz Haderach',
      'aliases' => 
      array (
        0 => 'Usul',
        1 => 'Muad\'Dib',
        2 => 'The Preacher',
      ),
    ),
    1 => 
    array (
      'name' => 'Duncan Idaho',
      'occupation' => 'Swordmaster',
    ),
  ),
  'summary' => 'Dune by Frank Herbert',
);
grasmash
fonte
Amando o conceito do expansor!
Guillaume Roderick
2

Em alguns idiomas, você pode usar uma biblioteca alternativa. Por exemplo, tampax é uma implementação de variáveis ​​de manipulação YAML:

const tampax = require('tampax');

const yamlString = `
dude:
  name: Arthur
weapon:
  favorite: Excalibur
  useless: knife
sentence: "{{dude.name}} use {{weapon.favorite}}. The goal is {{goal}}."`;

const r = tampax.yamlParseString(yamlString, { goal: 'to kill Mordred' });
console.log(r.sentence);

// output : "Arthur use Excalibur. The goal is to kill Mordred."
Arthur Lacoste
fonte
1

Seu exemplo é inválido apenas porque você escolheu um caractere reservado para iniciar seus escalares. Se você substituir *por outro caractere não reservado (eu costumo usar caracteres não ASCII para isso, pois eles raramente são usados ​​como parte de alguma especificação), você terminará com YAML perfeitamente legal:

paths:
  root: /path/to/root/
  patha: ♦root♦ + a
  pathb: ♦root♦ + b
  pathc: ♦root♦ + c

Isso será carregado na representação padrão para mapeamentos no idioma que seu analisador usa e não expande magicamente nada.
Para fazer isso, use um tipo de objeto localmente padrão, como no seguinte programa Python:

# coding: utf-8

from __future__ import print_function

import ruamel.yaml as yaml

class Paths:
    def __init__(self):
        self.d = {}

    def __repr__(self):
        return repr(self.d).replace('ordereddict', 'Paths')

    @staticmethod
    def __yaml_in__(loader, data):
        result = Paths()
        loader.construct_mapping(data, result.d)
        return result

    @staticmethod
    def __yaml_out__(dumper, self):
        return dumper.represent_mapping('!Paths', self.d)

    def __getitem__(self, key):
        res = self.d[key]
        return self.expand(res)

    def expand(self, res):
        try:
            before, rest = res.split(u'♦', 1)
            kw, rest = rest.split(u'♦ +', 1)
            rest = rest.lstrip() # strip any spaces after "+"
            # the lookup will throw the correct keyerror if kw is not found
            # recursive call expand() on the tail if there are multiple
            # parts to replace
            return before + self.d[kw] + self.expand(rest)
        except ValueError:
            return res

yaml_str = """\
paths: !Paths
  root: /path/to/root/
  patha: ♦root♦ + a
  pathb: ♦root♦ + b
  pathc: ♦root♦ + c
"""

loader = yaml.RoundTripLoader
loader.add_constructor('!Paths', Paths.__yaml_in__)

paths = yaml.load(yaml_str, Loader=yaml.RoundTripLoader)['paths']

for k in ['root', 'pathc']:
    print(u'{} -> {}'.format(k, paths[k]))

que imprimirá:

root -> /path/to/root/
pathc -> /path/to/root/c

A expansão é feita em tempo real e lida com definições aninhadas, mas você deve ter cuidado para não chamar a recursão infinita.

Ao especificar o dumper, você pode despejar o YAML original dos dados carregados, devido à expansão on-the-fly:

dumper = yaml.RoundTripDumper
dumper.add_representer(Paths, Paths.__yaml_out__)
print(yaml.dump(paths, Dumper=dumper, allow_unicode=True))

isso mudará a ordem das chaves de mapeamento. Se esse é um problema, você deve fazer self.dum CommentedMap(importado de ruamel.yaml.comments.py)

Anthon
fonte
0

Eu escrevi minha própria biblioteca em Python para expandir variáveis ​​sendo carregadas de diretórios com uma hierarquia como:

/root
 |
 +- /proj1
     |
     +- config.yaml
     |
     +- /proj2
         |
         +- config.yaml
         |
         ... and so on ...

A principal diferença aqui é que a expansão deve ser aplicada somente após o config.yamlcarregamento de todos os arquivos, onde as variáveis ​​do próximo arquivo podem substituir as variáveis ​​do anterior, portanto, o pseudocódigo deve ficar assim:

env = YamlEnv()
env.load('/root/proj1/config.yaml')
env.load('/root/proj1/proj2/config.yaml')
...
env.expand()

Como opção adicional, o xonshscript pode exportar as variáveis ​​resultantes para variáveis ​​de ambiente (consulte a yaml_update_global_varsfunção).

Os scripts:

https://sourceforge.net/p/contools/contools/HEAD/tree/trunk/Scripts/Tools/cmdoplib.yaml.py https://sourceforge.net/p/contools/contools/HEAD/tree/trunk/Scripts /Tools/cmdoplib.yaml.xsh

Prós :

  • simples, não suporta recursão e variáveis ​​aninhadas
  • pode substituir uma variável indefinida por um espaço reservado ( ${MYUNDEFINEDVAR}->*$/{MYUNDEFINEDVAR} )
  • pode expandir uma referência da variável de ambiente ( ${env:MYVAR})
  • pode substituir toda \\a /em uma variável de caminho ( ${env:MYVAR:path})

Contras :

  • não suporta variáveis ​​aninhadas, portanto, não é possível expandir valores em dicionários aninhados (algo como ${MYSCOPE.MYVAR}não está implementado)
  • não detecta recursão de expansão, incluindo recursão após um espaço reservado
Andry
fonte
0

Com Yglu , você pode escrever seu exemplo como:

paths:
  root: /path/to/root/
  patha: !? .paths.root + a
  pathb: !? .paths.root + b
  pathc: !? .paths.root + c

Disclaimer: Eu sou o autor de Yglu.

lbovet
fonte
É bom estar ciente de uma biblioteca que adiciona essa funcionalidade ao YAML
Dhiraj