Repository: litestar-org/litestar

Issue:
Bug: openapi render for multiple tags isn't consistent
**Describe the bug**
When the openapi renders tags from both a controller and a route it is not deterministic. This may not be a bug? But it surprised me so thought I'd raise it.

I'm unsure if I'm doing something crazy but for a project, we check in the generated json openapi schema so we can browse the API live in gitlab. I've recently added a tag to both a controller and a route in it. But because the order of the tags isn't consistent they are going to keep flip flopping as we have a pre-commit that generates the json to make sure it's up to date. I hope that ramble makes sense...

**To Reproduce**
```python
from typing import Dict                                           
                                                                  
from starlite import Starlite, Controller, get                    
                                                                  
class TestController(Controller):                                 
    tags = ["a"]                                                  
                                                                  
    @get("/", tags=["b"])                                         
    def hello_world(self) -> Dict[str, str]:                      
        """Handler function that returns a greeting dictionary."""
        return {"hello": "world"}                                 
                                                                  
                                                                  
app = Starlite(route_handlers=[TestController])                   
print(app.openapi_schema.paths["/"].get.tags)                     
```
If you run that multiple times, you will see you get either:
```python
['a', 'b']
```
or
```python
['b', 'a']
```

**Additional context**
I believe the problem is [here](https://github.com/starlite-api/starlite/blob/835749112e8364c1516f45973c924774aca22ca9/starlite/openapi/path_item.py#L59) as it forces construction of a new set. Sorting them before returning would be viable as there shouldn't be _too many_ tags and it's a one time thing I believe?

But as I said, it may not be a problem you care about as I could be doing something silly.



Discussion:
Let me know if this is correct and an okay approach and I'll make a PR with a test
So the issue is the order of tags? I'd assume we probably use a set somewhere, which will jumble the order. 
Yup 🙂 

> I believe the problem is [here](https://github.com/starlite-api/starlite/blob/835749112e8364c1516f45973c924774aca22ca9/starlite/openapi/path_item.py#L59) as it forces construction of a new set. Sorting them before returning would be viable as there shouldn't be too many tags and it's a one time thing I believe?
> Yup 🙂 
> 
> > I believe the problem is [here](https://github.com/starlite-api/starlite/blob/835749112e8364c1516f45973c924774aca22ca9/starlite/openapi/path_item.py#L59) as it forces construction of a new set. Sorting them before returning would be viable as there shouldn't be too many tags and it's a one time thing I believe?

Sure. Wanna add a PR?
Yeah, can do. I was just wondering if you do try to guarantee the repeatability of your schema generation from the same code - There may well be other places
>  if you do try to guarantee the repeatability of your schema generation from the same code

Afaik we currently don't have a strong guarantee for this, but imo it makes sense as a requirement. The schema should not change if the code doesn't change.


Resolving patch:
diff --git a/starlite/openapi/path_item.py b/starlite/openapi/path_item.py
--- a/starlite/openapi/path_item.py
+++ b/starlite/openapi/path_item.py
@@ -56,7 +56,7 @@ def extract_layered_values(
             tags.extend(layer.tags)
         if layer.security:
             security.extend(layer.security)
-    return list(set(tags)) if tags else None, security or None
+    return sorted(set(tags)) if tags else None, security or None
 
 
 def create_path_item(


Test patch:
diff --git a/tests/openapi/test_tags.py b/tests/openapi/test_tags.py
--- a/tests/openapi/test_tags.py
+++ b/tests/openapi/test_tags.py
@@ -23,7 +23,7 @@ class _Controller(Controller):
         path = "/controller"
         tags = ["controller"]
 
-        @get(tags=["handler"])
+        @get(tags=["handler", "a"])
         def _handler(self) -> Any:
             ...
 
@@ -50,8 +50,8 @@ def test_openapi_schema_handler_tags(openapi_schema: "OpenAPI") -> None:
 
 
 def test_openapi_schema_controller_tags(openapi_schema: "OpenAPI") -> None:
-    assert set(openapi_schema.paths["/controller"].get.tags) == {"handler", "controller"}  # type: ignore
+    assert openapi_schema.paths["/controller"].get.tags == ["a", "controller", "handler"]  # type: ignore
 
 
 def test_openapi_schema_router_tags(openapi_schema: "OpenAPI") -> None:
-    assert set(openapi_schema.paths["/router/controller"].get.tags) == {"handler", "controller", "router"}  # type: ignore
+    assert openapi_schema.paths["/router/controller"].get.tags == ["a", "controller", "handler", "router"]  # type: ignore