Repository: ansible-collections/community.general

Issue:
hashids filter
Hello,
It will be awesome for ansible to have hashids filter: https://hashids.org/

ISSUE TYPE

   * Feature Idea



Discussion:
Files identified in the description:
None

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
@peylight: Greetings! Thanks for taking the time to open this issue. In order for the community to handle your issue effectively, we need a bit more information.

Here are the items we could not find in your description:
- issue type
- ansible version
- component name

Please set the description of this issue with this template:
https://raw.githubusercontent.com/ansible/ansible/devel/.github/ISSUE_TEMPLATE/bug_report.md

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: issue_missing_data --->

Resolving patch:
diff --git a/plugins/filter/hashids.py b/plugins/filter/hashids.py
new file mode 100644
--- /dev/null
+++ b/plugins/filter/hashids.py
@@ -0,0 +1,97 @@
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2021, Andrew Pantuso (@ajpantuso) <ajpantuso@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.errors import (
+    AnsibleError,
+    AnsibleFilterError,
+    AnsibleFilterTypeError,
+)
+
+from ansible.module_utils.common.text.converters import to_native
+from ansible.module_utils.common.collections import is_sequence
+
+try:
+    from hashids import Hashids
+    HAS_HASHIDS = True
+except ImportError:
+    HAS_HASHIDS = False
+
+
+def initialize_hashids(**kwargs):
+    if not HAS_HASHIDS:
+        raise AnsibleError("The hashids library must be installed in order to use this plugin")
+
+    params = dict((k, v) for k, v in kwargs.items() if v)
+
+    try:
+        return Hashids(**params)
+    except TypeError as e:
+        raise AnsibleFilterError(
+            "The provided parameters %s are invalid: %s" % (
+                ', '.join(["%s=%s" % (k, v) for k, v in params.items()]),
+                to_native(e)
+            )
+        )
+
+
+def hashids_encode(nums, salt=None, alphabet=None, min_length=None):
+    """Generates a YouTube-like hash from a sequence of ints
+
+       :nums: Sequence of one or more ints to hash
+       :salt: String to use as salt when hashing
+       :alphabet: String of 16 or more unique characters to produce a hash
+       :min_length: Minimum length of hash produced
+    """
+
+    hashids = initialize_hashids(
+        salt=salt,
+        alphabet=alphabet,
+        min_length=min_length
+    )
+
+    # Handles the case where a single int is not encapsulated in a list or tuple.
+    # User convenience seems preferable to strict typing in this case
+    # Also avoids obfuscated error messages related to single invalid inputs
+    if not is_sequence(nums):
+        nums = [nums]
+
+    try:
+        hashid = hashids.encode(*nums)
+    except TypeError as e:
+        raise AnsibleFilterTypeError(
+            "Data to encode must by a tuple or list of ints: %s" % to_native(e)
+        )
+
+    return hashid
+
+
+def hashids_decode(hashid, salt=None, alphabet=None, min_length=None):
+    """Decodes a YouTube-like hash to a sequence of ints
+
+       :hashid: Hash string to decode
+       :salt: String to use as salt when hashing
+       :alphabet: String of 16 or more unique characters to produce a hash
+       :min_length: Minimum length of hash produced
+    """
+
+    hashids = initialize_hashids(
+        salt=salt,
+        alphabet=alphabet,
+        min_length=min_length
+    )
+    nums = hashids.decode(hashid)
+    return list(nums)
+
+
+class FilterModule(object):
+
+    def filters(self):
+        return {
+            'hashids_encode': hashids_encode,
+            'hashids_decode': hashids_decode,
+        }


Test patch:
diff --git a/tests/integration/targets/filter_hashids/aliases b/tests/integration/targets/filter_hashids/aliases
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/filter_hashids/aliases
@@ -0,0 +1,2 @@
+shippable/posix/group2
+skip/python2.6  # filters are controller only, and we no longer support Python 2.6 on the controller
diff --git a/tests/integration/targets/filter_hashids/runme.sh b/tests/integration/targets/filter_hashids/runme.sh
new file mode 100755
--- /dev/null
+++ b/tests/integration/targets/filter_hashids/runme.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+set -eux
+
+export ANSIBLE_TEST_PREFER_VENV=1  # see https://github.com/ansible/ansible/pull/73000#issuecomment-757012395; can be removed once Ansible 2.9 and ansible-base 2.10 support has been dropped
+source virtualenv.sh
+
+# Requirements have to be installed prior to running ansible-playbook
+# because plugins and requirements are loaded before the task runs
+
+pip install hashids
+
+ANSIBLE_ROLES_PATH=../ ansible-playbook runme.yml "$@"
diff --git a/tests/integration/targets/filter_hashids/runme.yml b/tests/integration/targets/filter_hashids/runme.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/filter_hashids/runme.yml
@@ -0,0 +1,3 @@
+- hosts: localhost
+  roles:
+    - { role: filter_hashids }
diff --git a/tests/integration/targets/filter_hashids/tasks/main.yml b/tests/integration/targets/filter_hashids/tasks/main.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/filter_hashids/tasks/main.yml
@@ -0,0 +1,58 @@
+####################################################################
+# WARNING: These are designed specifically for Ansible tests       #
+# and should not be used as examples of how to write Ansible roles #
+####################################################################
+
+- name: Test valid hashable inputs
+  assert:
+    that:
+      - "single_int | community.general.hashids_encode | community.general.hashids_decode == [single_int]"
+      - "int_list | community.general.hashids_encode | community.general.hashids_decode | list == int_list"
+      - "(1,2,3) | community.general.hashids_encode | community.general.hashids_decode == [1,2,3]"
+
+- name: Test valid parameters
+  assert:
+    that:
+      - "single_int | community.general.hashids_encode(salt='test') | community.general.hashids_decode(salt='test') == [single_int]"
+      - "single_int | community.general.hashids_encode(alphabet='1234567890abcdef') | community.general.hashids_decode(alphabet='1234567890abcdef') == [single_int]"
+      - "single_int | community.general.hashids_encode(min_length=20) | community.general.hashids_decode(min_length=20) == [single_int]"
+      - "single_int | community.general.hashids_encode(min_length=20) | length == 20"
+
+- name: Test valid unhashable inputs
+  assert:
+    that:
+      - "single_float | community.general.hashids_encode | community.general.hashids_decode == []"
+      - "arbitrary_string | community.general.hashids_encode | community.general.hashids_decode == []"
+
+- name: Register result of invalid salt
+  debug:
+    var: "invalid_input | community.general.hashids_encode(salt=10)"
+  register: invalid_salt_message
+  ignore_errors: true
+
+- name: Test invalid salt fails
+  assert:
+    that:
+      - invalid_salt_message is failed
+
+- name: Register result of invalid alphabet
+  debug:
+    var: "invalid_input | community.general.hashids_encode(alphabet='abc')"
+  register: invalid_alphabet_message
+  ignore_errors: true
+
+- name: Test invalid alphabet fails
+  assert:
+    that:
+      - invalid_alphabet_message is failed
+
+- name: Register result of invalid min_length
+  debug:
+    var: "invalid_input | community.general.hashids_encode(min_length='foo')"
+  register: invalid_min_length_message
+  ignore_errors: true
+
+- name: Test invalid min_length fails
+  assert:
+    that:
+      - invalid_min_length_message is failed
diff --git a/tests/integration/targets/filter_hashids/vars/main.yml b/tests/integration/targets/filter_hashids/vars/main.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/filter_hashids/vars/main.yml
@@ -0,0 +1,4 @@
+single_int: 1
+int_list: [1, 2, 3]
+single_float: [2.718]
+arbitrary_string: "will not hash"