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

Export title and version to openapi.yaml #128

Merged
merged 4 commits into from
Sep 8, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
13 changes: 11 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,13 @@ Acceptable will generate a JSON schema representation of the form for documentat
To generate API metadata, you should add 'acceptable' to INSTALLED_APPS. This
will provide an 'acceptable' management command::


./manage.py acceptable metadata > api.json # generate metadata

And also::

./manage.py acceptable api-version api.json # inspect the current version



Documentation (beta)
--------------------

Expand All @@ -109,6 +107,7 @@ This markdown is designed to rendered to html by

documentation-builder --base-directory docs


Includable Makefile
-------------------

Expand Down Expand Up @@ -150,3 +149,13 @@ Development
-----------

``make test`` and ``make tox`` should run without errors.

To run a single test module invoke::

python setup.py test --test-suite acceptable.tests.test_module

or::

tox -epy38 -- --test-suite acceptable.tests.test_module

...the latter runs "test_module" against Python 3.8 only.
45 changes: 35 additions & 10 deletions acceptable/openapi.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
"""
Helpers to translate acceptable metadata to OpenAPI specifications (OAS).
"""

from dataclasses import dataclass, field
from typing import Any

import yaml

from acceptable._service import APIMetadata

def _to_dict(source: Any):

def _to_dict(source: Any):
if hasattr(source, "_to_dict"):
return source._to_dict() # noqa
elif type(source) == dict:
Expand All @@ -21,17 +22,41 @@ def _to_dict(source: Any):
for item in source:
source_list.append(_to_dict(item))
return source_list
elif hasattr(source, "__dict__"):
source_dict = {}
for key, value in source.__dict__.items():
source_dict[key] = _to_dict(value)
return source_dict
lofidevops marked this conversation as resolved.
Show resolved Hide resolved
else:
return source


def dump(metadata, stream):
@dataclass
class OasInfo(object):
description: str = ""
version: str = ""
title: str = ""
tags: list = field(default_factory=lambda: [])
contact: dict = field(default_factory=lambda: {"name": "", "email": ""})


@dataclass
class OasRoot31(object):
openapi: str = "3.1.0"
lofidevops marked this conversation as resolved.
Show resolved Hide resolved
info: OasInfo = OasInfo()
servers: dict = field(default_factory=lambda: {})
paths: dict = field(default_factory=lambda: {})
components_schemas: dict = field(default_factory=lambda: {})


def dump(metadata: APIMetadata, stream):
lofidevops marked this conversation as resolved.
Show resolved Hide resolved
service_name = None
if len(metadata.services) == 1:
service_name = list(metadata.services.keys())[0]

# TODO: parse metadata as OpenAPI specification
oas = OasRoot31()
oas.info.title = service_name or ""
oas.info.version = metadata.current_version or ""

return yaml.safe_dump(
_to_dict(None),
stream,
default_flow_style=False,
encoding=None,
)
oas_to_dict = _to_dict(oas)
return yaml.safe_dump(oas_to_dict, stream, default_flow_style=False, encoding=None, )
lofidevops marked this conversation as resolved.
Show resolved Hide resolved
139 changes: 58 additions & 81 deletions acceptable/tests/test_main.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
# Copyright 2017 Canonical Ltd. This software is licensed under the
# GNU Lesser General Public License version 3 (see the file LICENSE).
import argparse
from collections import OrderedDict
import contextlib
from functools import partial
import io
import json
import os
import subprocess
import sys
import tempfile
import yaml
from collections import OrderedDict
from functools import partial

import testtools
import fixtures
import testtools
import yaml

from acceptable import __main__ as main
from acceptable import get_metadata
Expand Down Expand Up @@ -148,7 +148,7 @@ def my_view():
'request_schema': {'type': 'object'},
'response_schema': {'type': 'object'},
'params_schema': {'type': 'object'},
'introduced_at': 1,
'introduced_at': 1,
'title': 'Root',
}
}
Expand Down Expand Up @@ -234,7 +234,7 @@ def my_view():
'request_schema': {'type': 'object'},
'response_schema': {'type': 'object'},
'params_schema': {'type': 'object', 'properties': {'test': {'type': 'string'}}},
'introduced_at': 1,
'introduced_at': 1,
'title': 'Root',
}
}
Expand Down Expand Up @@ -299,7 +299,7 @@ def metadata(self):
},
'request_schema': {'request_schema': 1},
'response_schema': {'response_schema': 2},
'introduced_at': 1,
'introduced_at': 1,
'title': 'Api1',
}
return metadata
Expand Down Expand Up @@ -349,7 +349,7 @@ def metadata(self):
},
'request_schema': {'request_schema': 1},
'response_schema': {'response_schema': 2},
'introduced_at': 1,
'introduced_at': 1,
'title': 'Api1',
}
metadata['group']['apis']['api2'] = {
Expand All @@ -363,7 +363,7 @@ def metadata(self):
},
'request_schema': {'request_schema': 1},
'response_schema': {'response_schema': 2},
'introduced_at': 1,
'introduced_at': 1,
'title': 'Api2',
}
return metadata
Expand All @@ -376,14 +376,7 @@ def test_render_markdown_success(self):
iterator = main.render_markdown(self.metadata(), args)
output = OrderedDict((str(k), v) for k, v in iterator)

self.assertEqual(set([
'en/group.md',
'en/index.md',
'en/metadata.yaml',
'metadata.yaml',
]),
set(output),
)
self.assertEqual({'en/group.md', 'en/index.md', 'en/metadata.yaml', 'metadata.yaml'}, set(output))

top_level_md = yaml.safe_load(output['metadata.yaml'])
self.assertEqual(
Expand All @@ -393,11 +386,11 @@ def test_render_markdown_success(self):

md = yaml.safe_load(output['en/metadata.yaml'])
self.assertEqual({
'navigation': [
{'location': 'index.md', 'title': 'Index'},
{'location': 'group.md', 'title': 'Group'},
],
},
'navigation': [
{'location': 'index.md', 'title': 'Index'},
{'location': 'group.md', 'title': 'Group'},
],
},
lofidevops marked this conversation as resolved.
Show resolved Hide resolved
md
)

Expand All @@ -410,24 +403,17 @@ def test_render_markdown_undocumented(self):
iterator = main.render_markdown(m, args)
output = OrderedDict((str(k), v) for k, v in iterator)

self.assertEqual(set([
'en/group.md',
'en/index.md',
'en/metadata.yaml',
'metadata.yaml',
]),
set(output),
)
self.assertEqual({'en/group.md', 'en/index.md', 'en/metadata.yaml', 'metadata.yaml'}, set(output))

self.assertNotIn('api2', output['en/group.md'])

md = yaml.safe_load(output['en/metadata.yaml'])
self.assertEqual({
'navigation': [
{'location': 'index.md', 'title': 'Index'},
{'location': 'group.md', 'title': 'Group'},
],
},
'navigation': [
{'location': 'index.md', 'title': 'Index'},
{'location': 'group.md', 'title': 'Group'},
],
},
md
)

Expand All @@ -440,22 +426,15 @@ def test_render_markdown_deprecated_at(self):
iterator = main.render_markdown(m, args)
output = OrderedDict((str(k), v) for k, v in iterator)

self.assertEqual(set([
'en/group.md',
'en/index.md',
'en/metadata.yaml',
'metadata.yaml',
]),
set(output),
)
self.assertEqual({'en/group.md', 'en/index.md', 'en/metadata.yaml', 'metadata.yaml'}, set(output))

md = yaml.safe_load(output['en/metadata.yaml'])
self.assertEqual({
'navigation': [
{'location': 'index.md', 'title': 'Index'},
{'location': 'group.md', 'title': 'Group'},
],
},
'navigation': [
{'location': 'index.md', 'title': 'Index'},
{'location': 'group.md', 'title': 'Group'},
],
},
md
)

Expand All @@ -472,15 +451,8 @@ def test_render_markdown_multiple_groups(self):
iterator = main.render_markdown(metadata, args)
output = OrderedDict((str(k), v) for k, v in iterator)

self.assertEqual(set([
'en/group.md',
'en/group2.md',
'en/index.md',
'en/metadata.yaml',
'metadata.yaml',
]),
set(output),
)
self.assertEqual({'en/group.md', 'en/group2.md', 'en/index.md', 'en/metadata.yaml', 'metadata.yaml'},
set(output))
lofidevops marked this conversation as resolved.
Show resolved Hide resolved

top_level_md = yaml.safe_load(output['metadata.yaml'])
self.assertEqual(
Expand All @@ -490,12 +462,12 @@ def test_render_markdown_multiple_groups(self):

md = yaml.safe_load(output['en/metadata.yaml'])
self.assertEqual({
'navigation': [
{'location': 'index.md', 'title': 'Index'},
{'location': 'group.md', 'title': 'Group'},
{'location': 'group2.md', 'title': 'Group2'},
],
},
'navigation': [
{'location': 'index.md', 'title': 'Index'},
{'location': 'group.md', 'title': 'Group'},
{'location': 'group2.md', 'title': 'Group2'},
],
},
md
)

Expand All @@ -513,14 +485,7 @@ def test_render_markdown_group_omitted_with_undocumented(self):
iterator = main.render_markdown(metadata, args)
output = OrderedDict((str(k), v) for k, v in iterator)

self.assertEqual(set([
'en/group.md',
'en/index.md',
'en/metadata.yaml',
'metadata.yaml',
]),
set(output),
)
self.assertEqual({'en/group.md', 'en/index.md', 'en/metadata.yaml', 'metadata.yaml'}, set(output))

top_level_md = yaml.safe_load(output['metadata.yaml'])
self.assertEqual(
Expand All @@ -530,11 +495,11 @@ def test_render_markdown_group_omitted_with_undocumented(self):

md = yaml.safe_load(output['en/metadata.yaml'])
self.assertEqual({
'navigation': [
{'location': 'index.md', 'title': 'Index'},
{'location': 'group.md', 'title': 'Group'},
],
},
'navigation': [
{'location': 'index.md', 'title': 'Index'},
{'location': 'group.md', 'title': 'Group'},
],
},
md
)

Expand Down Expand Up @@ -612,10 +577,19 @@ def test_render_cmd_with_documentation_builder(self):
' Documentation: API foo at response_schema.foo_result.introduced_at'),
]

EXPECTED_OPENAPI_RESULT = [
"null\n",
"...\n",
]
EXPECTED_OPENAPI_RESULT = """components_schemas: {}
lofidevops marked this conversation as resolved.
Show resolved Hide resolved
info:
contact:
email: ''
name: ''
description: ''
tags: []
title: OpenApiSample
version: 5
openapi: 3.1.0
paths: {}
servers: {}
"""


class LintTests(testtools.TestCase):
Expand Down Expand Up @@ -655,7 +629,10 @@ def test_openapi_output(self):

# And the OpenAPI file contains the expected value
with open("examples/oas_testcase_openapi.yaml", "r") as f:
self.assertListEqual(EXPECTED_OPENAPI_RESULT, f.readlines())
result = f.readlines()

expected = EXPECTED_OPENAPI_RESULT.splitlines(keepends=True)
self.assertListEqual(expected, result)

# And we implicitly assume the files have not changed

Expand Down
Loading