# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from yapconf.items import YapconfDictItem
# flake8: noqa
HEADER = """# {app_name} Configuration
This document describes the configuration for {app_name}. Each section will
document a particular configuration value and its description. First,
though, we start with the possible sources.
This documentation was auto-generated by [yapconf.](https://github.com/loganasherjones/yapconf)
"""
CONFIG_HEADER = """## Configuration
This section outlines the various configuration items {app_name} supports.
"""
SOURCES_HEADER = """
## Sources
{app_name} configuration can be loaded from the below sources:
"""
SOURCE_TEMPLATE = """
#### {source_label}
{source_label} is of type {source_type_link}.
{source_type_description}
"""
ITEM_TEMPLATE = """### {name}
{description}
{attribute_table}
{item_options}
{long_description}
"""
SOURCE_TYPE_LINKS = {
"dict": "https://yapconf.readthedocs.io/en/stable/sources.html#dict",
"environment": "https://yapconf.readthedocs.io/en/stable/sources.html#environment",
"etcd": "https://yapconf.readthedocs.io/en/stable/sources.html#etcd",
"json": "https://yapconf.readthedocs.io/en/stable/sources.html#json",
"kubernetes": "https://yapconf.readthedocs.io/en/stable/sources.html#kubernetes",
"yaml": "https://yapconf.readthedocs.io/en/stable/sources.html#yaml",
}
VALID_JSON_LINK = "[valid JSON](https://www.json.org/)"
VALID_YAML_LINK = "[valid YAML](http://yaml.org/)"
DICT_DESCRIPTION = """
The dict source means that {app_name} will load configuration values from an
in-memory dictionary that cannot be modified externally. Usually, it is used as
a list of sensible defaults."""
ENVIRONMENT_DESCRIPTION = """
This environment source means that {app_name} will load configuration values
by using their `env_name` that is documented below."""
ETCD_DESCRIPTION = """
The etcd source means that {app_name} will load configuration values from the
`{key}` in etcd."""
JSON_DATA_DESCRIPTION = """
This json source means that {app_name} will load configuration values from an
in-memory string and cannot be modified. Usually it is used as a list of
sensible defaults."""
JSON_FILE_DESCRIPTION = """
This json source means that {app_name} will load configuration values from
`{filename}`. You may edit this file with {valid_link}."""
K8S_DESCRIPTION = """The kubernetes source indicates that the configuration
may be loaded from a kubernetes [ConfigMap](https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/).
Specifically, {app_name} will load the configuration from the ConfigMap named
`{config_map_name}` in the namespace `{config_map_namespace}`"""
K8S_KEY_DESCRIPTION = """
Additionally, {source_label} specified that `{key}` exists in the
`{config_map_name}` ConfigMap. `{key}` will be loaded as `{config_type}`.
You may modify this entry with {valid_link}."""
YAML_DESCRIPTION = """
The yaml source means that {app_name} will load configuration values from
`{filename}`. You may edit this file with {valid_link}"""
def _find_row_maxes(headers, rows):
maxes = {}
for key, value in headers.items():
maxes[key] = len(value)
for row in rows:
for key, value in row.items():
maxes[key] = max(maxes[key], len(value))
return maxes
def _get_padding(key, row_maxes, value, pad_value=" ", modifier=0):
return pad_value * (row_maxes[key] - len(value) + modifier)
def _build_row(row, row_maxes, row_keys, pad_value=" "):
row_value = "|"
for key in row_keys:
padding = _get_padding(key, row_maxes, row[key], pad_value)
row_value = row_value + " " + row[key] + padding + " |"
return row_value
def _build_separator(row_maxes, row_keys):
fake_row = row_maxes.copy()
for key, value in row_maxes.items():
fake_row[key] = ""
return _build_row(fake_row, row_maxes, row_keys, pad_value="-")
[docs]def build_markdown_table(headers, rows, row_keys=None):
"""Build a lined up markdown table.
Args:
headers (dict): A key -> value pairing fo the headers.
rows (list): List of dictionaries that contain all the keys listed in
the headers.
row_keys (list): A sorted list of keys to display
Returns:
A valid Markdown Table as a string.
"""
row_maxes = _find_row_maxes(headers, rows)
row_keys = row_keys or [key for key, value in headers.items()]
table = [
_build_row(headers, row_maxes, row_keys),
_build_separator(row_maxes, row_keys),
]
for row in rows:
table.append(_build_row(row, row_maxes, row_keys))
return "\n".join(table) + "\n"
def _generate_source_description(source, app_name, source_label):
format_kwargs = {"app_name": app_name}
template = ""
if source.type == "dict":
template = DICT_DESCRIPTION
elif source.type == "environment":
template = ENVIRONMENT_DESCRIPTION
elif source.type == "etcd":
template = ETCD_DESCRIPTION
format_kwargs["key"] = source.key
elif source.type == "json":
if source.filename:
template = JSON_FILE_DESCRIPTION
format_kwargs["filename"] = source.filename
format_kwargs["valid_link"] = VALID_JSON_LINK
else:
template = JSON_DATA_DESCRIPTION
elif source.type == "kubernetes":
template = K8S_DESCRIPTION
format_kwargs["config_map_name"] = source.name
format_kwargs["config_map_namespace"] = source.namespace
if source.key:
template = "\n".join([t for t in [template, K8S_KEY_DESCRIPTION]])
format_kwargs["source_label"] = source_label
format_kwargs["key"] = source.key
format_kwargs["config_type"] = source.config_type
if source.config_type == "yaml":
format_kwargs["valid_link"] = VALID_YAML_LINK
else:
format_kwargs["valid_link"] = VALID_JSON_LINK
elif source.type == "yaml":
template = YAML_DESCRIPTION
format_kwargs["filename"] = source.filename
format_kwargs["valid_link"] = VALID_YAML_LINK
return template.format(**format_kwargs)
def _generate_source_section(source_label, source, app_name):
source_type_link = "[%s](%s)" % (source.type, SOURCE_TYPE_LINKS[source.type],)
source_type_description = _generate_source_description(
source, app_name, source_label
)
return SOURCE_TEMPLATE.format(
source_label=source_label,
source_type_link=source_type_link,
source_type_description=source_type_description,
)
def _generate_item_table(item):
headers = {
"attribute": "Attribute",
"value": "Value",
}
if item.cli_support:
cli_names = item.cli_names
else:
cli_names = None
rows = [
{"attribute": "**item_type**", "value": "`%s`" % item.item_type,},
{"attribute": "**default**", "value": "`%s`" % item.default,},
{"attribute": "**env_name**", "value": "`%s`" % item.env_name,},
{"attribute": "**required**", "value": "`%s`" % item.required,},
{"attribute": "**cli_name**", "value": "`%s`" % cli_names,},
{"attribute": "**fallback**", "value": "`%s`" % item.fallback},
{"attribute": "**choices**", "value": "`%s`" % item.choices,},
]
return build_markdown_table(headers, rows, ["attribute", "value"])
def _generate_item_options(item, app_name):
options = []
if item.env_name:
options.append(
"You can set {name} from the environment by setting the "
"environment variable `{env_name}`".format(
name=item.fq_name, env_name=item.env_name
)
)
if item.cli_support:
cli_names = item.cli_names
options.append(
"You can set `{name}` from the command-line by specifying "
"`{cli_names}` at {app_name}'s entrypoint.".format(
name=item.fq_name, cli_names=cli_names, app_name=app_name,
)
)
if item.fallback:
option = (
"If `{name}` is not set in any of the sources listed, it will "
"attempt to fallback to the value set in `{fallback}`."
).format(name=item.fq_name, fallback=item.fallback)
if item.default:
option += (
" If neither the fallback nor the original key is found "
"then the value will fallback to the default of `{default}`"
).format(default=("%s" % item.default))
options.append(option)
elif item.default:
options.append(
(
"If `{name}` is not set in any of the sources listed, it will "
"fallback to the default value `{value}`".format(
name=item.fq_name, value=item.default
)
)
)
return "\n\n".join(options)
def _generate_item_section(item, app_name):
item_table = _generate_item_table(item)
item_options = _generate_item_options(item, app_name)
long_description = item.long_description or ""
return ITEM_TEMPLATE.format(
name=item.fq_name,
description=item.description or "No description provided.",
attribute_table=item_table,
item_options=item_options,
long_description=long_description,
)
def _get_table_row_from_item(item):
return {
"name": "[" + item.fq_name + "](#" + item.fq_name + ")",
"type": item.item_type,
"default": "%s" % item.default,
"description": "%s" % item.description,
}
def _generate_item_sections(items, app_name):
rows = []
item_sections = []
for item in items:
if isinstance(item, YapconfDictItem):
tmp_rows, tmp_sections = _generate_item_sections(
_sorted_dict_values(item.children), app_name
)
rows += tmp_rows
item_sections += tmp_sections
else:
rows.append(_get_table_row_from_item(item))
item_sections.append(_generate_item_section(item, app_name))
return rows, item_sections
def _sorted_dict_values(dictionary):
sorted_keys = sorted(list(dictionary))
return [dictionary[key] for key in sorted_keys]
[docs]def generate_markdown_doc(app_name, spec):
"""Generate Markdown Documentation for the given spec/app name.
Args:
app_name (str): The name of the application.
spec (YapconfSpec): A yapconf specification with sources loaded.
Returns (str):
A valid, markdown string representation of the documentation for
the given specification.
"""
# Apply standard headers.
sections = [
HEADER.format(app_name=app_name),
SOURCES_HEADER.format(app_name=app_name),
]
# Generate the sources section of the documentation
sorted_labels = sorted(list(spec.sources))
for label in sorted_labels:
sections.append(_generate_source_section(label, spec.sources[label], app_name))
# Generate the config section.
sections.append(CONFIG_HEADER.format(app_name=app_name))
table_rows, item_sections = _generate_item_sections(
_sorted_dict_values(spec.items), app_name
)
headers = {
"name": "Name",
"type": "Type",
"default": "Default",
"description": "Description",
}
sections.append(
build_markdown_table(
headers, table_rows, ["name", "type", "default", "description"],
)
)
for item_section in item_sections:
sections.append(item_section)
return "\n".join([section for section in sections])