blablabla1234678 / REST

REST API and client with HATEOAS
0 stars 0 forks source link

Add validation both server and client side #16

Open blablabla1234678 opened 1 month ago

blablabla1234678 commented 1 month ago

Validation can be only partial here because the documentation should not allow any external validation like if an email address is unique in the database or if a record exists with the user id, if a password exist in a compromised password service and so on. It must work only with the actual data we have and with the documentation. It should be extendable, customizable if possible, but it is not the first priority now.

blablabla1234678 commented 1 month ago

I wrote a few validators and I think it is hard to write a good one because of dependencies between the validation rules. There is a clear priority sometimes e.g. required comes first, type check second, in the case of number range check, in the case of string length check and so on... Another thing is dependencies between fields, invariants, etc. How to handle all of these?

blablabla1234678 commented 1 month ago

Currently the documentation looks like this:

    const docs = {
        main: {
            type: "Hyperlink",
            method: "get",
            uri: "/",
            response: {
                type: "Object"
            }
        },
        registerPerson: {
            type: "Hyperlink",
            method: "post",
            uri: "/people",
            request: {
                type: "FlatObject",
                items: {
                    name: {
                        type: "Name",
                        required: true
                    },
                    age: {
                        type: "Age",
                        required: true
                    },
                    gender: {
                        type: "Gender"
                    }
                }
            },
            response: {
                type: "Person"
            }
        },
        listPeople: {
            type: "Hyperlink",
            method: "get",
            uri: "/people?{page}",
            request: {
                type: "FlatObject",
                items: {
                    page: {
                        type: "Number"
                    }
                }
            },
            response: {
                type: "People"
            }
        },
        People: {
            type: "Array",
            items: {type: "Person"}
        },
        Person: {
            type: "Object",
            items: {
                id: {type: "Number"},
                name: {type: "Name"},
                age: {type: "Age"},
                gender: {type: "Gender"}
            }
        },
        Name: {
            type: "String",
            length: {
                min: 3,
                max: 255
            }
        },
        Age: {
            type: "Number",
            range: {
                min: 18,
                max: 150
            }
        },
        Gender: {
            type: "Number",
            alternatives: [1,2,3]
        }
    };
blablabla1234678 commented 1 month ago

We just need a portion of it to have something relative hard to validate:

        People: {
            type: "Array",
            items: {type: "Person"}
        },
        Person: {
            type: "Object",
            items: {
                id: {type: "Number"},
                name: {type: "Name"},
                age: {type: "Age"},
                gender: {type: "Gender"}
            }
        },
        Name: {
            type: "String",
            length: {
                min: 3,
                max: 255
            }
        },
        Age: {
            type: "Number",
            range: {
                min: 18,
                max: 150
            }
        },
        Gender: {
            type: "Number",
            alternatives: [1,2,3]
        }

On top of this the requests have required switches as well.

blablabla1234678 commented 1 month ago
        registerPerson: {
            type: "Hyperlink",
            method: "post",
            uri: "/people",
            request: {
                type: "FlatObject",
                items: {
                    name: {
                        type: "Name",
                        required: true
                    },
                    age: {
                        type: "Age",
                        required: true
                    },
                    gender: {
                        type: "Gender"
                    }
                }
            },
            response: {
                type: "Person"
            }
        },

I think validation must start by the hyperlinks. After all we validate requests and responses.

blablabla1234678 commented 1 month ago

Here the parser can help a lot:

        People: {
            type: "Array",
            items: {type: "Person"}
        },
        Person: {
            type: "Object",
            items: {
                id: {type: "Number"},
                name: {type: "Name"},
                age: {type: "Age"},
                gender: {type: "Gender"}
            }
        },
        Name: {
            type: "String",
            length: {
                min: 3,
                max: 255
            }
        },
        Age: {
            type: "Number",
            range: {
                min: 18,
                max: 150
            }
        },
        Gender: {
            type: "Number",
            alternatives: [1,2,3]
        }

the parsed version

{
    type: ["People", "Array"],
    items: {
        type: ["Person", "Object"],
        items: {
            id: {
                type: "Number"
            },
            name: {
                type: ["Name", "String"],
                length: {
                    min: 3,
                    max: 255
                }
            },
            age: {
                type: ["Age", "Number"],
                range: {
                    min: 18,
                    max: 150
                }
            },
            gender: {
                type: ["Gender", "Number"],
                alternatives: [1,2,3]
            }
        }
    }
}
blablabla1234678 commented 1 month ago

A valid response should look like this:

{
    items: [
        {
            id: 1,
            name: "Susanne Doyle",
            age: 36,
            gender: 2
        },
        {
            id: 2,
            name: "John Smith",
            age: 23,
            gender: 1
        },
    ]
}
blablabla1234678 commented 1 month ago

The validator goes through both the value and the documentation and validates the data. The current ValueIterator might be good for this job. It uses the documentation as the base of iteration and not the value. This was intentional.

callback(selector, definition, value);

The most problematic part with it that it is up to down. So first we apply the rules for the Person class, next we apply the rules for each properties. It would be nice to validate the properties first and when they are all ok, then apply the class rules. So a down to up iterator would be a better solution. It is hard to write such iterator from scratch. We can use the DocumentationIterator grab the selectors and run through the selectors in a reverse order to get such an iterator. It might worth the effort though it uses double loops. Another simpler solution is put the callback at the end of the code instead of at the beginning of the code. Though this can cause optimization issues because it means a tail call. I would rather stick with the reverse solution. Yet another thing here is iterating collections. Which a simple documentation iterator cannot do, it requires the value iterator. So we are stuck here a little bit. I think we should use the ReverseValueIterator and problem solved for now.

blablabla1234678 commented 1 month ago

Adding custom rules should be done by classes instead of selectors I think. For example we already have default type check for natives like Array, Object, FlatArray, FlatObject, Number, String, etc. We can add check for Person, People, Name, Age, Gender in the upper example. Since most of the rules are bound to a class this solves the issue of invariants. For example Name, Address, Localization, etc. all contain such invariants. Typically we use ValueObjects or Entities to describe them in the domain model and ViewModels are not that different in responses.

blablabla1234678 commented 1 month ago

One done. The other thing was validation rule priority. I don't think this must be something general. We could add a level or a dependency graph. Still the first very basic thing is hard to solve with those:

if (value !== undefined)
    return applyRules(dependentRules, value);
if (required)
    return ["required"];

Here the dependent rules are applied only if the value is set. Priority and dependency graph does not solve this. We need something like stop applying dependent rules when it fails. After that we can define a dependency graph, since I like it better than priority levels.

required.stopOnFailure
type -> required
type.stopOnFailure
length -> type:string,
range -> type:number

Should the meaning be context dependent or all rules should have a unique name? I think the unique name is simpler from modelling perspective.

Yet another thought that maybe the stopOnFailure is not the property of the required, but it is the property of the type. Something like we cannot continue if we have an error. Or maybe both? I think we can introduce checkpoints for it on the dependency graph.

required
    ->checkpoint(not null)
    ->type(type)
        ->checkpoint(type string)
            ->length(min,max)
        ->checkpoint(type number)
            ->range(min,max)

This one is solved as well. Now I think we can start to write code.