encode / apistar

The Web API toolkit. 🛠
https://docs.apistar.com
BSD 3-Clause "New" or "Revised" License
5.57k stars 411 forks source link

Is enabling `unique_items=True` useful for non-primitive types? #556

Closed freakabcd closed 5 years ago

freakabcd commented 6 years ago

If unique_items=True is set on a validator.Array() whose items are a non-primitive type, a http request to the handler under inspection crashes the server with an assertion error. I am wondering if something along the lines of validators.py

def make_hashable(self, element):
    assert (element is None) or \
        isinstance(element, (bool, int, float, str, list, dict)) or \
        hasattr(element, '__hash__')
    ...
    return hash(element)

to enable unique_items=True on custom types will be useful to users. Users should obviously be warned that they need to implement sane __eq__ and __hash__ methods. The check above is rather primitive, perhaps there are better ways to consider a type as hashable; @nedbat says to simply attempt the hashing as per https://stackoverflow.com/questions/3460650/asking-is-hashable-about-a-python-value/3460725#3460725 With the above fix, a very slightly modified example from https://github.com/encode/apistar/issues/507#issuecomment-390854838

from apistar import App, Route, types, validators
from apistar import TestClient

class Trip(types.Type):
    name = validators.String(min_length=3)

    def __eq__(self, other):
        if not isinstance(other, type(self)):
            return NotImplemented
        return self.name == other.name

    def __hash__(self):
        return hash(self.name)

class TripData(types.Type):
    trips = validators.Array(
        items=Trip,
        min_items=1,
        unique_items=True
    )

def calculate(validatedTrips: TripData):
    for trip in validatedTrips.trips:
        print(trip)

def post_request_to_calculate(client: TestClient, json: dict):
    response = client.post('/calculate', json=json)
    print(response)
    if response.status_code != 200:
        print(response.text)
    print(10*'-')

routes = [Route('/calculate', method='POST', handler=calculate)]
app = App(routes=routes)

if __name__ == '__main__':
    client = TestClient(app)
    post_request_to_calculate(client, {
        'trips': [{"name": "something"}, {"name": "something else"}]
    })
    post_request_to_calculate(client, {
        'trips': [{"name": "something"}, {"not_a_name": "something else"}]
    })

    post_request_to_calculate(client, {
        'trips': [
            {"name": "something"},
            {"name": "something else"},
            {"name": "something"}
        ]
    })

works as expected and identifies the third entry of the array violating the unique constraint. Output:

<Trip(name='something')>
<Trip(name='something else')>
<Response [200]>
----------
<Response [400]>
{"trips":{"1":{"name":"This field is required."}}}
----------
<Response [400]>
{"trips":{"2":"This item is not unique."}}
----------
tomchristie commented 5 years ago

TypeSystem now handles all the validation https://github.com/encode/typesystem (And yes - it gets uniquness correct on composite types, such as lists and dicts)