Repository: StackStorm/st2

Issue:
Issue on calling action-alias match in CLI
calling 
`st2 action-alias match "st2 pack install" `
Gives the error
```
2016-11-30 04:02:38,984 140340458909360 INFO hooks [-] 25e7cbee-ec68-4040-9adc-f3f82f11774c -  POST /v1/actionalias/match with filters={} (remote_addr='127.0.0.1',method='POST',filters={},request_id='25e7cbee-ec68-4040-9adc-f3f82f11774c',path='/v1/actionalias/match')
2016-11-30 04:02:38,985 140340458909360 AUDIT auth [-] Token provided in headers
2016-11-30 04:02:38,986 140340458909360 AUDIT auth [-] Token with id "583e4f54c4da5f27da3c32ea" is validated.
2016-11-30 04:02:38,990 140340458909360 WARNING base [-] Type definition for 'action_alias_match_api' argument of 'match' is missing.
2016-11-30 04:02:38,990 140340458909360 INFO resource [-] GET all /v1/actionalias/match with filters={'order_by': ['pack', 'name']} (offset=0,limit='None',filters={'order_by': ['pack', 'name']},sort=[])
2016-11-30 04:02:38,996 140340458909360 ERROR hooks [-] API call failed: 'representation'
Traceback (most recent call last):
  File "/opt/stackstorm/st2/local/lib/python2.7/site-packages/pecan/core.py", line 631, in __call__
    self.invoke_controller(controller, args, kwargs, state)
  File "/opt/stackstorm/st2/local/lib/python2.7/site-packages/pecan/core.py", line 531, in invoke_controller
    result = controller(*args, **kwargs)
  File "/opt/stackstorm/st2/local/lib/python2.7/site-packages/st2common/rbac/decorators.py", line 61, in func_wrapper
    return func(*args, **kwargs)
  File "/opt/stackstorm/st2/local/lib/python2.7/site-packages/st2common/models/api/base.py", line 284, in callfunction
    raise e
KeyError: 'representation' (_exception_data={},_exception_class='KeyError',_exception_message="'representation'")
2016-11-30 04:02:38,997 140340458909360 ERROR hooks [-] Traceback (most recent call last):
  File "/opt/stackstorm/st2/local/lib/python2.7/site-packages/pecan/core.py", line 631, in __call__
    self.invoke_controller(controller, args, kwargs, state)
  File "/opt/stackstorm/st2/local/lib/python2.7/site-packages/pecan/core.py", line 531, in invoke_controller
    result = controller(*args, **kwargs)
  File "/opt/stackstorm/st2/local/lib/python2.7/site-packages/st2common/rbac/decorators.py", line 61, in func_wrapper
    return func(*args, **kwargs)
  File "/opt/stackstorm/st2/local/lib/python2.7/site-packages/st2common/models/api/base.py", line 284, in callfunction
    raise e
KeyError: 'representation'
Traceback (most recent call last):
  File "/opt/stackstorm/st2/local/lib/python2.7/site-packages/pecan/core.py", line 631, in __call__
    self.invoke_controller(controller, args, kwargs, state)
  File "/opt/stackstorm/st2/local/lib/python2.7/site-packages/pecan/core.py", line 531, in invoke_controller
    result = controller(*args, **kwargs)
  File "/opt/stackstorm/st2/local/lib/python2.7/site-packages/st2common/rbac/decorators.py", line 61, in func_wrapper
    return func(*args, **kwargs)
  File "/opt/stackstorm/st2/local/lib/python2.7/site-packages/st2common/models/api/base.py", line 284, in callfunction
    raise e
KeyError: 'representation'
2016-11-30 04:02:38,997 140340458909360 INFO hooks [-] 25e7cbee-ec68-4040-9adc-f3f82f11774c - POST /v1/actionalias/match result={
    "faultstring": "Internal Server Error"
} (result='{\n    "faultstring": "Internal Server Error"\n}',request_id='25e7cbee-ec68-4040-9adc-f3f82f11774c',status_code='500 Internal Server Error',remote_addr='127.0.0.1',path='/v1/actionalias/match',method='POST')
```


Discussion:
Think the error is raised here:
https://github.com/StackStorm/st2/blob/master/st2common/st2common/util/actionalias_matching.py#L68

Full disclosure, the code was ported from something I wrote.  I hit this error yesterday and discussed it with @emedvedev.  I'll quote it here:

>carlos 10:11 PM is it a considered a bug if a pack provides an actionalias with a format which contains a dict with only a display key but no representation?  If there no representation shouldn't it be simple string in the format?

> emedvedev 11:42 PM carlos good question :slightly_smiling_face: And no, it is not an error, although it is a bit hacky. Basically, `display` gets registered in help strings, and `representation` gets registered as a Hubot command. Normally you would pair them, but it’s possible to use one without the other. In the alias you’re referring to it’s just a way to get two help strings for a single command, because the full format might be a bit complicated

I'm wondering how this case should be handled when more than one full display/representation dict is provided in the format list. Which representation should the additional help string is associated with?

The other issue here is that malformed format element shouldn't cause 500 Internal Server error, they should probably be put inside a try catch and skipped when encountered.

I can provide a pull request if necessary.
**TL;DR: `display` or `representation` being empty should not be considered an error; return the `representation` string that matched, not its `display`.**

Let me start by giving context. Before display/representation format objects were implemented in v1.2, `format` was a list of strings like `st2 pack install {{ packs }}`, and each line would serve as both a command and a help string. Super simple, but not ideal in a few cases: 
1. When having multiple format strings didn't require multiple help entries. Explicit is better than implicit, of course, but for people who prefer a less pythonic approach this use case is often legit.
2. When a command required more explicit format in help. When you look at `st2 pack install {{ packs }}`, can you tell if it requires the pack list to be comma-separated or space-separated? Should it be a name or a URL?
3. When a command required multiple help strings. Again, `st2 pack install` is a good example: a pack can be a name, or it can be a URL, and it can also be a mixed list of both, and in future versions we might want to introduce a version separator. Trying to explain this in a single help string would get hella confusing.
4. When a user wanted to hide a command from help. Sometimes people want to have "tribal knowledge" aliases just for fun, for instance.

When regex-based format strings were introduced, 2 and 4 became more prominent. If you had something like `(st2|stackstorm) packs? install (me some )? {{ packs }} (kthxbye)?`, you didn't want all this weird stuff in help.

So that's how a string's `display` in help got separated from its `representation`. At the time it made sense to make `representation` a list under one `display`: most people use `display`/`representation` to hide multiple format strings under one help entry. That satisfied 1 and 2. To satisfy 3 and 4, `display` and `representation` were both made optional: if you have `representation` but not `display`, the commands will be registered but hidden from help (4), if you have `display` but not `representation`, a help string will be added but no command will be registered (3).

To the issue at hand:
1. When generating help strings, you should take all `display` strings and ignore `representation`.
2. When registering/matching commands, you should use `representation` and ignore `display`.
3. In `st2 action-alias match`, when telling which string matched, it's better to return a `representation` string, not the corresponding `display`, because that would be more useful when debugging matching patterns (if you have one display with five representations, showing display isn't really useful: you want to debug which one of the five matched).

Needless to say, this is confusing. I'm only explaining this because @nzlosh will hit all possible corner cases while writing his module, and he should know the reasoning behind some of the weirder stuff we have. :)

@nzlosh despite the super-confusing explanation, this should be a fairly easy PR if you want to try it — all it takes is returning `representation` that matched instead of `display`. :)

Resolving patch:
diff --git a/st2common/st2common/util/actionalias_matching.py b/st2common/st2common/util/actionalias_matching.py
--- a/st2common/st2common/util/actionalias_matching.py
+++ b/st2common/st2common/util/actionalias_matching.py
@@ -39,16 +39,20 @@ def list_format_strings_from_aliases(aliases):
     for alias in aliases:
         for format_ in alias.formats:
             display, representations = normalise_alias_format_string(format_)
-            patterns.extend([(display, representation) for representation in representations])
+            if display and len(representations) == 0:
+                patterns.extend([(display, [])])
+            else:
+                patterns.extend([(display, representation) for representation in representations])
     return patterns
 
 
 def normalise_alias_format_string(alias_format):
     '''
-    StackStorm action aliases can have two types;
-        1. A simple string holding the format
-        2. A dictionary which hold numerous alias format "representation(s)"
-           With a single "display" for help about the action alias.
+    StackStorm action aliases come in two forms;
+        1. A string holding the format, which is also used as the help string.
+        2. A dictionary containing "display" and/or "representation" keys.
+           "representation": a list of numerous alias format "representation(s)"
+           "display": a help string to be displayed.
     This function processes both forms and returns a standardized form.
 
     :param alias_format: The alias format
@@ -64,8 +68,10 @@ def normalise_alias_format_string(alias_format):
         display = alias_format
         representation.append(alias_format)
     elif isinstance(alias_format, dict):
-        display = alias_format['display']
-        representation = alias_format['representation']
+        display = alias_format.get('display')
+        representation = alias_format.get('representation') or []
+        if isinstance(representation, six.string_types):
+            representation = [representation]
     else:
         raise TypeError("alias_format '%s' is neither a dictionary or string type."
                         % repr(alias_format))


Test patch:
diff --git a/st2common/tests/unit/test_util_actionalias_matching.py b/st2common/tests/unit/test_util_actionalias_matching.py
--- a/st2common/tests/unit/test_util_actionalias_matching.py
+++ b/st2common/tests/unit/test_util_actionalias_matching.py
@@ -65,6 +65,34 @@ def test_list_format_strings_from_aliases_with_display(self, mock):
         self.assertEqual(result[2][0], "How do I feel? I feel... {{status}}!")
         self.assertEqual(result[2][1], "How do I feel? I feel... {{status}}!")
 
+    def test_list_format_strings_from_aliases_with_display_only(self, mock):
+        ALIASES = [
+            MemoryActionAliasDB(name='andy',
+                                ref='the_goonies.1', formats=[{'display': 'Watch this.'}]),
+            MemoryActionAliasDB(name='andy', ref='the_goonies.2',
+                                formats=[{'display': "He's just like his {{relation}}."}])
+        ]
+        result = matching.list_format_strings_from_aliases(ALIASES)
+        self.assertEqual(len(result), 2)
+        self.assertEqual(result[0][0], 'Watch this.')
+        self.assertEqual(result[0][1], [])
+        self.assertEqual(result[1][0], "He's just like his {{relation}}.")
+        self.assertEqual(result[1][1], [])
+
+    def test_list_format_strings_from_aliases_with_representation_only(self, mock):
+        ALIASES = [
+            MemoryActionAliasDB(name='data', ref='the_goonies.1', formats=[
+                {'representation': "That's okay daddy. You can't hug a {{object}}."}]),
+            MemoryActionAliasDB(name='mr_wang', ref='the_goonies.2', formats=[
+                {'representation': 'You are my greatest invention.'}])
+        ]
+        result = matching.list_format_strings_from_aliases(ALIASES)
+        self.assertEqual(len(result), 2)
+        self.assertEqual(result[0][0], None)
+        self.assertEqual(result[0][1], "That's okay daddy. You can't hug a {{object}}.")
+        self.assertEqual(result[1][0], None)
+        self.assertEqual(result[1][1], 'You are my greatest invention.')
+
     def test_normalise_alias_format_string(self, mock):
         result = matching.normalise_alias_format_string(
             'Quite an experience to live in fear, isn\'t it?')