Welcome to support for indentation with spaces in to_xml plugin (#192)

* Welcome to support for indentation with spaces in to_xml plugin

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Changelog fragment

* Unit tests for:

* Indent with spaces and explicit indent_width (4)
* Invalid indent

I also slightly modified VALID_DATA to produce output with indentation,
so existing test_valid_data and OUTPUT has been updated accordingly.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix changelog section name to minor_changes

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
pull/222/head
Federico Chiacchiaretta 2022-11-04 19:13:13 +01:00 committed by GitHub
parent 2725ed3056
commit 0aee02eb97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 167 additions and 7 deletions

View File

@ -0,0 +1,5 @@
---
minor_changes:
- >-
to_xml - Added support for using spaces to indent an XML doc via a new
`indent` parameter.

View File

@ -73,6 +73,47 @@ Parameters
<div>Conversion library to use within the filter plugin.</div> <div>Conversion library to use within the filter plugin.</div>
</td> </td>
</tr> </tr>
<tr>
<td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
<b>indent</b>
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
<div style="font-size: small">
<span style="color: purple">string</span>
</div>
</td>
<td>
<ul style="margin: 0; padding: 0"><b>Choices:</b>
<li><div style="color: blue"><b>tabs</b>&nbsp;&larr;</div></li>
<li>spaces</li>
</ul>
</td>
<td>
</td>
<td>
<div>The character used for indentation (defaults to tabs).</div>
</td>
</tr>
<tr>
<td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
<b>indent_width</b>
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
<div style="font-size: small">
<span style="color: purple">integer</span>
</div>
</td>
<td>
<b>Default:</b><br/><div style="color: blue">4</div>
</td>
<td>
</td>
<td>
<div>The number of spaces to use to indent output data.</div>
<div>This option is only used when indent=&quot;spaces&quot;, otherwise it is ignored.</div>
<div>When indent=&quot;tabs&quot;, a single tab is always used for indentation.</div>
</td>
</tr>
</table> </table>
<br/> <br/>
@ -149,6 +190,38 @@ Examples
# Cisco-IOS-XR-ifmgr-cfg\">\n\t<interface-configuration></interface-configuration>\n</interface-configurations>" # Cisco-IOS-XR-ifmgr-cfg\">\n\t<interface-configuration></interface-configuration>\n</interface-configurations>"
# } # }
#### example3 with indent='spaces' and indent_width=2
- name: Define JSON data
ansible.builtin.set_fact:
data:
"interface-configurations":
"@xmlns": "http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg"
"interface-configuration":
- debug:
msg: "{{ data|ansible.utils.to_xml(indent='spaces', indent_width=2) }}"
# TASK [Define JSON data ] *************************************************************************
# task path: /Users/amhatre/ansible-collections/playbooks/test_utils_json_to_xml.yaml:5
# ok: [localhost] => {
# "ansible_facts": {
# "data": {
# "interface-configurations": {
# "@xmlns": "http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg",
# "interface-configuration": null
# }
# }
# },
# "changed": false
# }
# TASK [debug] ***********************************************************************************************************
# task path: /Users/amhatre/ansible-collections/playbooks/test_utils_json_to_xml.yaml:13
# Loading collection ansible.utils from /Users/amhatre/ansible-collections/collections/ansible_collections/ansible/utils
# ok: [localhost] => {
# "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<interface-configurations xmlns=\"http://cisco.com/ns/yang/
# Cisco-IOS-XR-ifmgr-cfg\">\n <interface-configuration></interface-configuration>\n</interface-configurations>"
# }

View File

@ -35,6 +35,19 @@ DOCUMENTATION = """
- Conversion library to use within the filter plugin. - Conversion library to use within the filter plugin.
type: str type: str
default: xmltodict default: xmltodict
indent:
description:
- The character used for indentation (defaults to tabs).
type: str
default: tabs
choices: ["tabs", "spaces"]
indent_width:
description:
- The number of spaces to use to indent output data.
- This option is only used when indent="spaces", otherwise it is ignored.
- When indent="tabs", a single tab is always used for indentation.
type: int
default: 4
""" """
EXAMPLES = r""" EXAMPLES = r"""
@ -104,6 +117,37 @@ EXAMPLES = r"""
# Cisco-IOS-XR-ifmgr-cfg\">\n\t<interface-configuration></interface-configuration>\n</interface-configurations>" # Cisco-IOS-XR-ifmgr-cfg\">\n\t<interface-configuration></interface-configuration>\n</interface-configurations>"
# } # }
#### example3 with indent='spaces' and indent_width=2
- name: Define JSON data
ansible.builtin.set_fact:
data:
"interface-configurations":
"@xmlns": "http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg"
"interface-configuration":
- debug:
msg: "{{ data|ansible.utils.to_xml(indent='spaces', indent_width=2) }}"
# TASK [Define JSON data ] *************************************************************************
# task path: /Users/amhatre/ansible-collections/playbooks/test_utils_json_to_xml.yaml:5
# ok: [localhost] => {
# "ansible_facts": {
# "data": {
# "interface-configurations": {
# "@xmlns": "http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg",
# "interface-configuration": null
# }
# }
# },
# "changed": false
# }
# TASK [debug] ***********************************************************************************************************
# task path: /Users/amhatre/ansible-collections/playbooks/test_utils_json_to_xml.yaml:13
# Loading collection ansible.utils from /Users/amhatre/ansible-collections/collections/ansible_collections/ansible/utils
# ok: [localhost] => {
# "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<interface-configurations xmlns=\"http://cisco.com/ns/yang/
# Cisco-IOS-XR-ifmgr-cfg\">\n <interface-configuration></interface-configuration>\n</interface-configurations>"
# }
""" """
from ansible.errors import AnsibleFilterError from ansible.errors import AnsibleFilterError
@ -123,7 +167,7 @@ except ImportError:
@pass_environment @pass_environment
def _to_xml(*args, **kwargs): def _to_xml(*args, **kwargs):
"""Convert the given data from json to xml.""" """Convert the given data from json to xml."""
keys = ["data", "engine"] keys = ["data", "engine", "indent", "indent_width"]
data = dict(zip(keys, args[1:])) data = dict(zip(keys, args[1:]))
data.update(kwargs) data.update(kwargs)
aav = AnsibleArgSpecValidator(data=data, schema=DOCUMENTATION, name="to_xml") aav = AnsibleArgSpecValidator(data=data, schema=DOCUMENTATION, name="to_xml")

View File

@ -35,18 +35,26 @@ def _raise_error(msg):
raise AnsibleFilterError(error) raise AnsibleFilterError(error)
def to_xml(data, engine): def to_xml(data, engine, indent, indent_width):
"""Convert data which is in json to xml" """Convert data which is in json to xml"
:param data: The data passed in (data|to_xml(...)) :param data: The data passed in (data|to_xml(...))
:type data: xml :type data: xml
:param engine: Conversion library default=xmltodict :param engine: Conversion library default=xmltodict
:param indent: Indent char default='tabs'
:param indent_width: Indent char multiplier default=4
""" """
indent_char = "\t"
if indent == "spaces":
indent_char = " " * indent_width
if engine == "xmltodict": if engine == "xmltodict":
if not HAS_XMLTODICT: if not HAS_XMLTODICT:
_raise_error("Missing required library xmltodict") _raise_error("Missing required library xmltodict")
try: try:
res = xmltodict.unparse(data, pretty=True) res = xmltodict.unparse(data, pretty=True, indent=indent_char)
except Exception: except Exception:
_raise_error("Input json is not valid") _raise_error("Input json is not valid")
return res return res

View File

@ -18,11 +18,21 @@ from ansible_collections.ansible.utils.plugins.filter.to_xml import _to_xml
INVALID_DATA = '<netconf-state xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring">' INVALID_DATA = '<netconf-state xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring">'
VALID_DATA = { VALID_DATA = {
"interface-configurations": {"@xmlns": "http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg"}, "interface-configurations": {
"@xmlns": "http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg",
"key1": "value1",
},
} }
OUTPUT = """<?xml version="1.0" encoding="utf-8"?> OUTPUT_TABS = """<?xml version="1.0" encoding="utf-8"?>
<interface-configurations xmlns="http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg"></interface-configurations>""" <interface-configurations xmlns="http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg">
\t<key1>value1</key1>
</interface-configurations>"""
OUTPUT_SPACES = """<?xml version="1.0" encoding="utf-8"?>
<interface-configurations xmlns="http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg">
<key1>value1</key1>
</interface-configurations>"""
class TestToXml(unittest.TestCase): class TestToXml(unittest.TestCase):
@ -44,7 +54,7 @@ class TestToXml(unittest.TestCase):
self.maxDiff = None self.maxDiff = None
args = ["", VALID_DATA, "xmltodict"] args = ["", VALID_DATA, "xmltodict"]
result = _to_xml(*args) result = _to_xml(*args)
self.assertEqual(result, OUTPUT) self.assertEqual(result, OUTPUT_TABS)
def test_args(self): def test_args(self):
"""Check passing invalid argspec""" """Check passing invalid argspec"""
@ -65,3 +75,23 @@ class TestToXml(unittest.TestCase):
with self.assertRaises(AnsibleError) as error: with self.assertRaises(AnsibleError) as error:
_to_xml(*args, **kwargs) _to_xml(*args, **kwargs)
self.assertIn("engine: test is not supported", str(error.exception)) self.assertIn("engine: test is not supported", str(error.exception))
def test_indent_with_spaces(self):
"""Check passing indent with spaces and default indent_width"""
self.maxDiff = None
args = ["", VALID_DATA, "xmltodict", "spaces", 4]
result = _to_xml(*args)
self.assertEqual(result, OUTPUT_SPACES)
def test_invalid_indent(self):
"""Check passing invalid indent value"""
# missing required arguments
args = ["", VALID_DATA, "xmltodict", "test"]
kwargs = {}
with self.assertRaises(AnsibleError) as error:
_to_xml(*args, **kwargs)
self.assertIn(
"value of indent must be one of: tabs, spaces, got: test",
str(error.exception),
)