Skip to content

Commit

Permalink
buildchain/targets: add a YAML Renderer
Browse files Browse the repository at this point in the history
Add a new serialization format: YAML.

This can be used as base for an SLS serializer.

In order to handle long string payload and binary content we add two
helpers (`YAMLDocument.text` and `YAMLDocument.bytestring`) that use
custom renderers in order to have a readable encoding (using `|` string
block literal) and right encoding (use Base64 for binary contents).

Refs: #2070
Signed-off-by: Sylvain Laperche <[email protected]>
  • Loading branch information
slaperche-scality committed Dec 9, 2019
1 parent 132d975 commit 7f34c58
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 4 deletions.
4 changes: 2 additions & 2 deletions buildchain/buildchain/targets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from buildchain.targets.repository import (
Repository, RPMRepository, DEBRepository
)
from buildchain.targets.serialize import Renderer, SerializedData
from buildchain.targets.serialize import Renderer, SerializedData, YAMLDocument
from buildchain.targets.template import TemplateFile

# For mypy, see `--no-implicit-reexport` documentation.
Expand All @@ -29,6 +29,6 @@
'Package', 'RPMPackage', 'DEBPackage',
'RemoteImage',
'Repository', 'RPMRepository', 'DEBRepository',
'Renderer', 'SerializedData',
'Renderer', 'SerializedData', 'YAMLDocument',
'TemplateFile',
]
60 changes: 58 additions & 2 deletions buildchain/buildchain/targets/serialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

"""Targets to write files from Python objects."""

import base64
import enum
import json
from pathlib import Path
from typing import Any, Callable, Mapping
from typing import Any, Callable, Dict, Mapping

import yaml

from buildchain import types
from buildchain import utils
Expand All @@ -29,18 +32,34 @@ def render_envfile(variables: Mapping[str, str], filepath: Path) -> None:
fp.write('\n')


def render_yaml(data: Any, filepath: Path) -> None:
"""Serialize an object as YAML to a given file path."""
with filepath.open('w', encoding='utf-8') as fp:
dumper = yaml.SafeDumper(fp, sort_keys=False)
dumper.add_representer(YAMLDocument.Literal, _literal_representer)
dumper.add_representer(YAMLDocument.ByteString, _bytestring_representer)
try:
dumper.open()
dumper.represent(data)
dumper.close()
finally:
dumper.dispose()


class Renderer(enum.Enum):
"""Supported rendering methods for `SerializedData` targets."""
JSON = 'JSON'
ENV = 'ENV'
YAML = 'YAML'


class SerializedData(base.AtomicTarget):
"""Serialize an object into a file with a specific renderer."""

RENDERERS = {
RENDERERS : Dict[Renderer, Callable[[Any, Path], None]] = {
Renderer.JSON: render_json,
Renderer.ENV: render_envfile,
Renderer.YAML: render_yaml,
}

def __init__(
Expand Down Expand Up @@ -95,3 +114,40 @@ def _render(self) -> Callable[[Any, Path], None]:
def _run(self) -> None:
"""Render the file."""
self._render(self._data, self._dest)


# YAML {{{


class YAMLDocument():
"""A YAML document, with an optional preamble (like a shebang)."""
class Literal(str):
"""A large block of text, to be rendered as a block scalar."""

class ByteString(bytes):
"""A binary string, to be rendered as a base64-encoded literal."""

@classmethod
def text(cls, value: str) -> 'YAMLDocument.Literal':
"""Cast the value to a Literal."""
return cls.Literal(value)

@classmethod
def bytestring(cls, value: bytes) -> 'YAMLDocument.ByteString':
"""Cast the value to a ByteString."""
return cls.ByteString(value)


def _literal_representer(dumper: yaml.BaseDumper, data: Any) -> Any:
scalar = yaml.representer.SafeRepresenter.represent_str(dumper, data)
scalar.style = '|'
return scalar


def _bytestring_representer(dumper: yaml.BaseDumper, data: Any) -> Any:
return _literal_representer(
dumper, base64.encodebytes(data).decode('utf-8')
)


# }}}

0 comments on commit 7f34c58

Please sign in to comment.