Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add documentation generation functionality #367

Merged
merged 13 commits into from
Mar 21, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/rpdk/core/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def generate(_args):
project = Project()
project.load()
project.generate()
project.generate_docs()

LOG.warning("Generated files for %s", project.type_name)

Expand Down
1 change: 1 addition & 0 deletions src/rpdk/core/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ def init(args):

project.init(type_name, language)
project.generate()
project.generate_docs()

LOG.warning("Initialized a new project in %s", project.root.resolve())

Expand Down
138 changes: 138 additions & 0 deletions src/rpdk/core/project.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import copy
import json
import logging
import os
import shutil
import zipfile
from pathlib import Path
from tempfile import TemporaryFile
Expand Down Expand Up @@ -293,6 +296,141 @@ def submit(
f, endpoint_url, region_name, role_arn, use_role, set_default
)

def generate_docs(self):
# generate the docs folder that contains documentation based on the schema
docs_path = "{}/docs".format(self.root)
iann0036 marked this conversation as resolved.
Show resolved Hide resolved

if not self.type_info or not self.schema or "properties" not in self.schema:
LOG.warning(
"Could not generate schema docs due to missing type info or schema"
)
return
iann0036 marked this conversation as resolved.
Show resolved Hide resolved

if os.path.isdir(docs_path):
LOG.debug("Docs directory already exists, recreating...")
shutil.rmtree(docs_path)
os.mkdir(docs_path)
iann0036 marked this conversation as resolved.
Show resolved Hide resolved

LOG.debug("Writing generated docs")

docs_schema = copy.deepcopy(
self.schema
) # take care not to overwrite master schema
iann0036 marked this conversation as resolved.
Show resolved Hide resolved

for propname in docs_schema["properties"]:
docs_schema["properties"][propname] = self._set_docs_properties(
propname, docs_schema["properties"][propname], []
)

ref = None
if "primaryIdentifier" in docs_schema:
if len(docs_schema["primaryIdentifier"]) == 1:
ref = docs_schema["primaryIdentifier"][0].split("/").pop()

getatt = []
if "additionalIdentifiers" in docs_schema:
for identifierptr in docs_schema["additionalIdentifiers"]:
if len(identifierptr) == 1:
attshortname = identifierptr[0].split("/").pop()
attdescription = "Returns the <code>{}</code> value.".format(
attshortname
)
attpath = identifierptr[0].replace(
"/properties/", ""
) # multi-depth getatt possible?
if (
attpath in docs_schema["properties"]
and "description" in docs_schema["properties"][attpath]
):
attdescription = docs_schema["properties"][attpath][
"description"
]
getatt.append({"name": attshortname, "description": attdescription})

template = self.env.get_template("docs-readme.yml")

contents = template.render(
type_name=self.type_name, schema=docs_schema, ref=ref, getatt=getatt
)
readme_path = Path("{}/README.md".format(docs_path))
iann0036 marked this conversation as resolved.
Show resolved Hide resolved
self.safewrite(readme_path, contents)

def _set_docs_properties(self, propname, prop, proppath):
proppath.append(propname)

if "$ref" in prop:
def_name = prop["$ref"].replace(
"#/definitions/", ""
) # primitive, will miss complex/external definitions
if "definitions" in self.schema and def_name in self.schema["definitions"]:
merged = {}
merged.update(self.schema["definitions"][def_name])
merged.update(prop) # properties overwrites definition
prop = merged

if (
"createOnlyProperties" in self.schema
and "/properties/{}".format("/".join(proppath))
in self.schema["createOnlyProperties"]
):
prop["createonly"] = True

basic_type_mappings = {
"string": "String",
"number": "Double",
"boolean": "Boolean",
}

prop["jsontype"] = "Unknown"
prop["yamltype"] = "Unknown"
prop["longformtype"] = "Unknown"

if prop["type"] in basic_type_mappings:
prop["jsontype"] = basic_type_mappings[prop["type"]]
prop["yamltype"] = basic_type_mappings[prop["type"]]
prop["longformtype"] = basic_type_mappings[prop["type"]]
elif prop["type"] == "array":
prop["arrayitems"] = self._set_docs_properties(
propname, prop["items"], copy.copy(proppath)
)
prop["jsontype"] = "[ " + prop["arrayitems"]["jsontype"] + ", ... ]"
prop["yamltype"] = "\n - " + prop["arrayitems"]["longformtype"]
prop["longformtype"] = "List of " + prop["arrayitems"]["longformtype"]
elif prop["type"] == "object":
template = self.env.get_template("docs-subproperty.yml")
docs_path = "{}/docs".format(self.root)

for subpropname in prop["properties"]:
prop["properties"][subpropname] = self._set_docs_properties(
subpropname, prop["properties"][subpropname], copy.copy(proppath),
)

subproperty_name = " ".join(proppath)
subproperty_filename = "-".join(proppath).lower() + ".md"

contents = template.render(
type_name=self.type_name,
subproperty_name=subproperty_name,
schema=prop,
)
readme_path = Path("{}/{}".format(docs_path, subproperty_filename))
self.safewrite(readme_path, contents)

prop["jsontype"] = (
'<a href="' + subproperty_filename + '">' + propname + "</a>"
)
prop["yamltype"] = (
'<a href="' + subproperty_filename + '">' + propname + "</a>"
)
prop["longformtype"] = (
'<a href="' + subproperty_filename + '">' + propname + "</a>"
)

if "enum" in prop:
prop["allowedvalues"] = prop["enum"]

return prop

def _upload(
self, fileobj, endpoint_url, region_name, role_arn, use_role, set_default
): # pylint: disable=too-many-arguments, too-many-locals
Expand Down
103 changes: 103 additions & 0 deletions src/rpdk/core/templates/docs-readme.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# {{ type_name }}
{% if schema.description %}

{{ schema.description }}
{% endif %}

## Syntax

To declare this entity in your AWS CloudFormation template, use the following syntax:

### JSON

<pre>
{
"Type" : "{{ type_name }}",
"Properties" : {
{% if schema.properties %}
{% for propname, prop in schema.properties.items() %}
"<a href="#{{ propname.lower() }}" title="{{ propname }}">{{ propname }}</a>" : <i>{{ prop.jsontype }}</i>{% if not loop.last %},{% endif %}

{% endfor %}
{% endif %}
}
}
</pre>

### YAML

<pre>
Type: {{ type_name }}
Properties:
{% if schema.properties %}
{% for propname, prop in schema.properties.items() %}
<a href="#{{ propname.lower() }}" title="{{ propname }}">{{ propname }}</a>: <i>{{ prop.yamltype }}</i>
{% endfor %}
{% endif %}
</pre>
{% if schema.properties %}

## Properties

{% for propname, prop in schema.properties.items() %}
#### {{ propname }}
{% if prop.description %}

{{ prop.description }}
{% endif %}

_Required_: {% if propname in schema.required %}Yes{% else %}No{% endif %}


_Type_: {{ prop.longformtype }}
{% if prop.allowedvalues %}

_Allowed Values_: <code>{{ "</code> | <code>".join(prop.allowedvalues) }}</code>
{% endif %}
{% if prop.minLength %}

_Minimum_: <code>{{ prop.minLength }}</code>
{% endif %}
{% if prop.maxLength %}

_Maximum_: <code>{{ prop.maxLength }}</code>
{% endif %}
{% if prop.pattern %}

_Pattern_: <code>{{ prop.pattern }}</code>
{% endif %}

{% if prop.createonly %}
_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
{% else %}
_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
{% endif %}

{% endfor %}
{% endif %}
{% if getatt|length > 0 or ref %}
## Return Values
{% if ref %}

### Ref

When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the {{ ref }}.
{% endif %}
{% if getatt|length > 0 %}

### Fn::GetAtt

The `Fn::GetAtt` intrinsic function returns a value for a specified attribute of this type. The following are the available attributes and sample return values.

For more information about using the `Fn::GetAtt` intrinsic function, see [Fn::GetAtt](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html).

{% for att in getatt %}
#### {{ att.name }}
{% if att.description %}

{{ att.description }}
{% endif %}

{% endfor %}
{% endif %}
{% endif %}
72 changes: 72 additions & 0 deletions src/rpdk/core/templates/docs-subproperty.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# {{ type_name }} {{ subproperty_name }}
{% if schema.description %}

{{ schema.description }}
{% endif %}

## Syntax

To declare this entity in your AWS CloudFormation template, use the following syntax:

### JSON

<pre>
{
{% if schema.properties %}
{% for propname, prop in schema.properties.items() %}
"<a href="#{{ propname.lower() }}" title="{{ propname }}">{{ propname }}</a>" : <i>{{ prop.jsontype }}</i>{% if not loop.last %},{% endif %}

{% endfor %}
{% endif %}
}
</pre>

### YAML

<pre>
{% if schema.properties %}
{% for propname, prop in schema.properties.items() %}
<a href="#{{ propname.lower() }}" title="{{ propname }}">{{ propname }}</a>: <i>{{ prop.yamltype }}</i>
{% endfor %}
{% endif %}
</pre>
{% if schema.properties %}

## Properties

{% for propname, prop in schema.properties.items() %}
#### {{ propname }}
{% if prop.description %}

{{ prop.description }}
{% endif %}

_Required_: {% if propname in schema.required %}Yes{% else %}No{% endif %}


_Type_: {{ prop.longformtype }}
{% if prop.allowedvalues %}

_Allowed Values_: <code>{{ "</code> | <code>".join(prop.allowedvalues) }}</code>
{% endif %}
{% if prop.minLength %}

_Minimum_: <code>{{ prop.minLength }}</code>
{% endif %}
{% if prop.maxLength %}

_Maximum_: <code>{{ prop.maxLength }}</code>
{% endif %}
{% if prop.pattern %}

_Pattern_: <code>{{ prop.pattern }}</code>
{% endif %}

{% if prop.createonly %}
_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement)
{% else %}
_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
{% endif %}

{% endfor %}
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"typeName": "AWS::Valid::TypeName",
"description": "a test schema",
"properties": {
"property1": {
"type": "string",
"$ref": "#/definitions/invalid"
}
},
"primaryIdentifier": [
"/properties/property1"
],
"additionalProperties": false
}
Loading