Repository: mirumee/ariadne

Issue:
Unexpected Snake Case for Acronyms
The snake case conversion of the `snake_case_fallback_resolvers` yields unexpected results for words with multiple uppercase letters in a row, e.g.
 - `getHTTPResponse` is converted to `get_h_t_t_p_response`, or
 - `externalID` is converted to "external_i_d`.  

These are unlikely names for python attributes and I would expect the resolver to look for `get_http_response` / `external_id` instead.

Possible implementations for the camel to snake case conversions are discussed here: https://stackoverflow.com/questions/1175208/elegant-python-function-to-convert-camelcase-to-snake-case
Unexpected Snake Case for Acronyms
The snake case conversion of the `snake_case_fallback_resolvers` yields unexpected results for words with multiple uppercase letters in a row, e.g.
 - `getHTTPResponse` is converted to `get_h_t_t_p_response`, or
 - `externalID` is converted to "external_i_d`.  

These are unlikely names for python attributes and I would expect the resolver to look for `get_http_response` / `external_id` instead.

Possible implementations for the camel to snake case conversions are discussed here: https://stackoverflow.com/questions/1175208/elegant-python-function-to-convert-camelcase-to-snake-case


Discussion:
PR: https://github.com/mirumee/ariadne/pull/452
The change in #452 will convert `GetHTTPResponse` to `get_httpresponse`, as the case I was trying to fix was `InterfaceMACs` - which, if following  a rule that would return `get_http_response`, would yield `interface_ma_cs`.

Off the top of my head I can't think of an algorithm that would work for both (without some dubious heuristics).
#452 is still good because it will convert `CanonicalURL` to `canonical_url`, reducing number of edge cases and surprises for Ariadne's users.

At the end of the day there's always `set_alias('GetHTTPResponse', 'get_http_response')`.
That is definitely an improvement.

I would argue the `ACRONYMOtherStuff-> acronym_other_stuff` case is more likely than `ACROnym->acronym` or `ACRONYMs -> acronyms`. But that is purely my gut feeling, I have no statistical data and I might be wrong.
I agree. I'd split `ABCDEFghiJklMNOpq` into `ABCDE`, `Fghi`, `Jkl`, `MN`, `Opq` (with the option to override `MACs` using custom aliases).
Another issue I'm currently running into: names like `phase_alpha2`, i. e. with a number at the end. Although very difficult to handle: should this result in `phase_alpha_2` or is it part of the name as in `phase_alpha2`.    
`set_alias()` helps here a lot.
PR: https://github.com/mirumee/ariadne/pull/452
The change in #452 will convert `GetHTTPResponse` to `get_httpresponse`, as the case I was trying to fix was `InterfaceMACs` - which, if following  a rule that would return `get_http_response`, would yield `interface_ma_cs`.

Off the top of my head I can't think of an algorithm that would work for both (without some dubious heuristics).
#452 is still good because it will convert `CanonicalURL` to `canonical_url`, reducing number of edge cases and surprises for Ariadne's users.

At the end of the day there's always `set_alias('GetHTTPResponse', 'get_http_response')`.
That is definitely an improvement.

I would argue the `ACRONYMOtherStuff-> acronym_other_stuff` case is more likely than `ACROnym->acronym` or `ACRONYMs -> acronyms`. But that is purely my gut feeling, I have no statistical data and I might be wrong.
I agree. I'd split `ABCDEFghiJklMNOpq` into `ABCDE`, `Fghi`, `Jkl`, `MN`, `Opq` (with the option to override `MACs` using custom aliases).
Another issue I'm currently running into: names like `phase_alpha2`, i. e. with a number at the end. Although very difficult to handle: should this result in `phase_alpha_2` or is it part of the name as in `phase_alpha2`.    
`set_alias()` helps here a lot.

Resolving patch:
diff --git a/ariadne/utils.py b/ariadne/utils.py
--- a/ariadne/utils.py
+++ b/ariadne/utils.py
@@ -6,20 +6,29 @@
 
 
 def convert_camel_case_to_snake(graphql_name: str) -> str:
+    # pylint: disable=too-many-boolean-expressions
+    max_index = len(graphql_name) - 1
+    lowered_name = graphql_name.lower()
+
     python_name = ""
-    for i, c in enumerate(graphql_name.lower()):
-        if (
-            i > 0
-            and (
-                all(
-                    (
-                        c != graphql_name[i],
-                        graphql_name[i - 1] != "_",
-                        graphql_name[i - 1] == python_name[-1],
-                    )
-                )
+    for i, c in enumerate(lowered_name):
+        if i > 0 and (
+            # testWord -> test_word
+            (
+                c != graphql_name[i]
+                and graphql_name[i - 1] != "_"
+                and graphql_name[i - 1] == python_name[-1]
+            )
+            # TESTWord -> test_word
+            or (
+                i < max_index
+                and graphql_name[i] != lowered_name[i]
+                and graphql_name[i + 1] == lowered_name[i + 1]
             )
-            or all((c.isdigit(), graphql_name[i - 1].isdigit() is False))
+            # test134 -> test_134
+            or (c.isdigit() and not graphql_name[i - 1].isdigit())
+            # 134test -> 134_test
+            or (not c.isdigit() and graphql_name[i - 1].isdigit())
         ):
             python_name += "_"
         python_name += c


Test patch:
diff --git a/tests/test_case_convertion_util.py b/tests/test_camel_case_to_snake_case_convertion.py
similarity index 78%
rename from tests/test_case_convertion_util.py
rename to tests/test_camel_case_to_snake_case_convertion.py
--- a/tests/test_case_convertion_util.py
+++ b/tests/test_camel_case_to_snake_case_convertion.py
@@ -1,3 +1,5 @@
+import pytest
+
 from ariadne import convert_camel_case_to_snake
 
 
@@ -41,8 +43,16 @@ def test_no_underscore_added_if_previous_character_is_an_underscore():
 
 
 def test_no_underscore_added_if_previous_character_is_uppercase():
-    assert convert_camel_case_to_snake("testWithUPPERPart") == "test_with_upperpart"
-
-
-def test_digits_are_treated_as_word():
-    assert convert_camel_case_to_snake("testWith365InIt") == "test_with_365_in_it"
+    assert convert_camel_case_to_snake("testWithUPPERPart") == "test_with_upper_part"
+
+
+@pytest.mark.parametrize(
+    ("test_str", "result"),
+    [
+        ("testWith365InIt", "test_with_365_in_it"),
+        ("365testWithInIt", "365_test_with_in_it"),
+        ("testWithInIt365", "test_with_in_it_365"),
+    ],
+)
+def test_digits_are_treated_as_word(test_str, result):
+    assert convert_camel_case_to_snake(test_str) == result