Output_required when you may not return anything


#1

Hi there!

I am still a bit confused about the usage of output_required and output_optional. At the moment all my services have the attributes that are mandatory in the schema set as items in the tuple assigned to output_required in a given service, and all the rest as output_optional.

But when I have a service such as validate, which validates a username and a password, I have two scenarios:

  • The login exists and has username, password and maybe some other optional attributes, so it works fine.
  • The login does not exist, therefore the output_required attributes force Zato to return an error 500 due to the exception raised.

This is the code:

class Login(Base):
    __tablename__ = 'login'
 
   id = Column(Integer, primary_key=True)
    username = Column(String(20), index=True, unique=True, nullable=False)
    password = Column(String(255), nullable=False)
    name = Column(String(50), index=True)
    surname = Column(String(50), index=True)
    email = Column(String(255), index=True, unique=True)
    is_admin = Column(Boolean, default=False)


class Validate(Service):
    """Service class to validate credentials."""
    """Channel /genesisng/logins/validate."""

    class SimpleIO:
        input_required = ('username', AsIs('password'))
        output_required = ('id', 'username')
        output_optional = ('name', 'surname', 'email', 'is_admin')
        skip_empty_keys = True

    def handle(self):
        conn = self.user_config.genesisng.database.connection
        username = self.request.input.username
        password = self.request.input.password

        with closing(self.outgoing.sql.get(conn).session()) as session:
            result = session.query(Login).\
                filter(and_(Login.username == username,
                            Login.password == password)).\
                one_or_none()

            if result:
                self.response.status_code = OK
                self.response.payload = result
            else:
                self.response.status_code = NOT_FOUND

What should be the correct approach when coding this type of services (validate, get, etc.)? I thought I got it right from this other post, but clearly not (enough).

Thanks in advance.


#2

The way I usually model I/O of such services is this:

class SimpleIO:
    input_required = 'username', 'password'
    output_required = 'is_valid', 'username'
    output_optional = 'id', 'name', 'surname', 'email', 'is_admin'
    skip_empty_keys = True

Which expresses the idea of:

  • Both a username and password are required on input
  • The is_valid flag is always given on output
  • Username is returned too, likely rewritten from input though it could be moved to the optional part as well, to be returned only if is_valid is True
  • The rest is always optional and will be returned only if is_valid is True

There is no way to express dependencies in SimpleIO, e.g. this and that field will be retuned if another one has a value such and such.

This could be added in theory but finding a syntax convenient enough would be the most difficult part. Then, SimpleIO is used to serialize API specifications to OpenAPI or WSDL that do not support such conditional expressions, so there would not be much benefit in doing it.


#3

I understand what you mean. I had planned on adding a metadata key to each dictionary returned, to be used to add information such as page number, count, etc. I’ll sleep over this and think of adding the is_valid field you propose.

Hmmm… I understand and agree with what you say, but I believe my case could be considered different, as I am talking about an all or nothing situation: either you have result(s) to return, or you do not. Example:

  • A valid username and password means we return a status code of 200 and a dictionary with the login class attributes.
  • An invalid username and password means we return a status code of 404 and no response.

Now a different scenario would be the following:

  • A valid username and password means we return a status code of 200 and a dictionary with the login model class, maybe with an extra metadata key in it.
  • An invalid username and password means we return a status code of 404 and nothing else, or maybe a dictionary with an extra error key in it (with a code and a description).

I see your point in the second case, but when you really don’t have anything to return (empty body, e.g. a List service with parameters in the query string that worked fine but just produced no results), then everything becomes optional. Not a problem, to be honest, as it is still useful to check and filter out attributes, just thinking out load to see what comes out :slight_smile:

I still have to think about adding the error and metadata keys to the returned dictionaries, so I’ll keep iterating on this issue on my prototype and come back to you maybe in some weeks.

Thanks, once more, for the feedback.


#4

This is correct but this is really what that service does. It seems to be more of a Login one - it returns user information in case credentials are valid or an error message if they are not.

An alternative would be a Login service that accepted credentials, returned a session key and then a separate service like GetUser which accepted the session key and returned details of a user associated with such a key.

Returning basic user details on login, if only to say ‘Hello {username}’ is such a common requirement though that a Login service should really return them. But at this moment, it begins to fulfil two distinct roles, validation and returning user data so the latter needs to be optional.

From my perspective, SimpleIO is just a way to express ideas about I/O in a declarative form with convenient serialization to wire-level formats, e.g. JSON. I probably never think of I/O in SimpleIO terms first, its definitions are only a way to write down what is designed elsewhere (UML, XSD, Jira diagrams etc.) What I mean is that if a service is designed to return user data only if credentials are valid then, by design, all of the user data must be optional on output, there is nothing wrong with it.

That all said, what I think would be helpful is a way to express the idea that a ‘user_basic_data’ key can be optionally returned but if it is returned then it will contain keys username, is_admin and so on, some of them optional and some not. But this needs to wait for a rewrite of SimpleIO to be completed - then you will be able to declare Dict elements with some of the keys optional and some required. Naturally, it will not be a completely flat structure on output then.


#5

I agree with your reasoning. I think that we are heading or rolling into the multilevel dicts and lists topic we talked about a few weeks ago. :wink:

I believe for now I’ll turn every attribute optional and keep testing (still so many areas of Zato to test, heh!).

The next stage in terms of responses is being shaped in my mind as follows:

{
    response: {
        result: {
            // attributes of the model class, or a list of
        },
        error: {
            // code and description
        },
        metadata: {
            // page number, element count, etc.
        }
}

Where result may not exist and, instead, all model class attributes to be returned would be at the root (right under response). I may wait for the rewrite of SimpleIO, depending on how much time it takes me to test the rest of Zato components (cache, scheduler, message queue, security, etc.).

Cheers, @dsuch!