Skip to content
This repository has been archived by the owner on May 3, 2020. It is now read-only.

RFC: programming approach #3

Open
mvesper opened this issue Apr 29, 2015 · 1 comment
Open

RFC: programming approach #3

mvesper opened this issue Apr 29, 2015 · 1 comment
Assignees
Milestone

Comments

@mvesper
Copy link
Contributor

mvesper commented Apr 29, 2015

RFC Programming approach

The circulation module is going to be rewritten from scratch in Invenio 2, making it more flexible and covering more library use cases. The purpose of this RFC is to discuss current development stages regarding the programming approach.

While discussing the possible use cases for the circulation module, it became apparent that the module is basically about managing states of different entities and triggering events based on those changes. Along with the fact that the statuses of items vary among the instances managing a library, the following approach is proposed:

The two entities having varying states, Item and Request (or LoanCycle, naming to be discussed), would be managed by using a configuration file using the following pattern:

{
    "on_shelf":
        {
            "from": 
                [
                    {
                        "status": "on_loan",
                        "attempt_enter":
                            {
                                "function": attempt_enter_shelf_func,
                                "needed_attributes": []
                            },
                        "on_enter": enter_shelf_func

                    }
                ],
            "to":
                [
                    {
                        "status": "on_loan",
                        "action_name": "loan",
                        "attempt_leave":
                            {
                                "function": attempt_leave_shelf_func,
                                "needed_attributes": []
                            },
                        "on_leave": leave_shelf_func,

                    }
                ]
        },
    "on_loan":
        {
            "from": 
                [
                    {
                        "status": "on_shelf",
                        "attempt_enter":
                            {
                                "function": attempt_enter_loan_func,
                                "needed_attributes": []
                            },
                        "on_enter": enter_loan_func

                    }
                ],
            "to":
                [
                    {
                        "status": "on_shelf",
                        "action_name": "return",
                        "attempt_leave":
                            {
                                "function": attempt_leave_loan_func,
                                "needed_attributes": []
                            },
                        "on_leave": leave_loan_func,

                    }
                ]
        }
}

This configuration file defines every possible transition between statuses and defines what is supposed to happen during those changes.

Example: Switching from "on_shelf" to "on_loan"
While being in status "on_shelf", one possible transition is to go to the status "on_loan". When this transition is triggered, the following functions are executed:

  • "on_shelf" -> "to" -> "attempt_leave" -> "function" will check the needed parameters to leave "on_shelf" and go to "on_loan"
  • "on_shelf" -> "to" -> "on_leave" will trigger every action associated with leaving "on_shelf" to "on_loan"
  • "on_loan" -> "from" -> "attempt_enter" -> "function" will check the needed parameters to enter the "on_loan" status coming from "on_shelf"
  • "on_loan" -> "from" -> "on_leave" will trigger every action associated with with entering "on_loan" from "on_shelf"

The classes Item and Request would then provide a very generic interface like the following:

class Item/Request(object):
    @classmethod
    def new(cls, ...):
        """Creates a new entity."""

    @classmethod
    def get(cls, id):
        """Gets and entity by ID."""

    @classmethod
    def get_all_functions(cls):
        """Gets all available functions triggering a transition."""

    @classmethod
    def get_all_statuses(cls):
        """Gets all available statuses."""

    def run(self, function_name, **kwargs):
        """Runs a transition."""

    def get_available_functions(self):
        """Gets the available functions triggering a transition from the current status."""

Following this approach the needed statuses and workflows can be configured and the demands for logging events can be satisfied.

UPDATE 18.05.2015

After playing around with that approach and the presented configuration file it became apparent that the config approach is a little to elaborate. It basically led to a lot of redundancy and was therefore hard to read. For example the loan action, would require a section in "on_shelf" -> "to" -> "on_loan" as well as a section in "on_loan" -> "from" -> "on_shelf". Additionally, this redundancy provided pretty little value. Sticking to the loan action example: The approach would trigger four function calls, and the question arose: Is this really necessary?

So, even though it moves a little bit away from the "state machine approach", this new configuration file style is proposed:

{
    "on_shelf":
        {
            "loan":
                {
                    "new_status": "on_loan",
                    "parameters": {"user": user.User},
                    "validate_func": validate_loan_item,
                    "func": loan_item
                },
            "lose_from_shelf":
                {
                    "new_status": "missing",
                    "parameters": {},
                    "validate_func": validate_lose,
                    "func": lose
                },
            "request":
                {
                    "new_status": "on_shelf",
                    "parameters": {"user": user.User,
                                   "date": datetime.datetime},
                    "validate_func": validate_request,
                    "func": request
                }
        },
    "on_loan":
        {
            "return":
                {
                    "new_status": "on_shelf",
                    "parameters": {},
                    "validate_func": validate_return_item,
                    "func": return_item 
                },
            "lose_from_loan":
                {
                    "new_status": "missing",
                    "parameters": {},
                    "validate_func": validate_lose,
                    "func": lose
                },
            "request":
                {
                    "new_status": "on_loan",
                    "parameters": {"user": user.User,
                                   "date": datetime.datetime},
                    "validate_func": validate_request,
                    "func": request
                }
        },
    "missing":
        {
            "return":
                {
                    "new_status": "on_shelf",
                    "parameters": {},
                    "validate_func": validate_return_missing,
                    "func": return_missing
                }
        }
}

It's a more direct mapping from a given status to the available actions related to it. The validate_func and func parameter work the same way as the "attempt__" -> "function" and "on__" functions in the previous configuration, whereas parameters corresponds to "needed_attributes", but moved up one level, since they are needed for both function calls anyways.

Also the interface changed a bit (Should become more similar to the Invenio 2 way):

class CirculationObject(object):

    @classmethod
    def new(cls, status='new'):
        """Creates a new entity."""

    @classmethod
    def get(cls, id):
        """Gets an entity by ID."""

    @classmethod
    def query(cls, **kwargs):
        """Gets entities by utilizing the provides search capabilities."""

    @classmethod
    def get_all_functions(cls):
         """Gets all available functions triggering a transition."""

    @classmethod
    def get_all_statuses(cls):
        """Gets all available statuses."""

    def get_function_parameters(self, function_name, status=None):
        """Returns the required parameters for a given function."""

    def validate_run(self, function_name, **kwargs):
        """Runs the *validate_func* function."""

    def run(self, function_name, **kwargs):
        """Runs a transition."""

    def get_available_functions(self):
        """Gets the available functions triggering a transition from the current status."""

UPDATE 17.07.2015

As it turned out, a function based approach causes some difficulties regarding the validation, since some of the information gathered during the validation step can be reused in the actual run. Therefore, objects are proposed to store those kind of information.

item_config = {
    "on_shelf":
        {
            "loan": LoanConfig,
            "lose_from_shelf": LoseConfig,
            "request": RequestConfig
        },
    "on_loan":
        {
            "return": ReturnConfig,
            "lose_from_loan": LoseConfig,
            "request": RequestConfig
        },
    "missing":
        {
            "return": ReturnMissingConfig
        }
}

An example Config object looks like this:

class ReturnConfig(ConfigBase):
    new_status = 'on_shelf'
    parameters = ['item']

    def run(self, _storage, item=None, **kwargs):
        clc = CirculationLoanCycle.search(item=item,
                                          current_status='on_loan')[0]
        clc.run('return', _storage=_storage)

    def check_item(self, item=None, **kwargs):
        if not item:
            raise Exception('There needs to be an item')

The new_status attributes triggers the status change after execution of the script, while parameters is supposed to make the interaction easier.

There is currently also some magic happening, which executes every method starting with 'check' during the validation step and then gathers the exceptions. This is supposed to be utilized later on in the user interface to provide information for the user about incorrect settings. But there should be a debug mode for developers, since this exception gathering removes some information currently.

@mvesper mvesper changed the title RFC Programming approach RFC: programming approach Apr 29, 2015
@jirikuncar jirikuncar added this to the v1.0.0 milestone Aug 15, 2016
@david-caro
Copy link

Is this still relevant?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

3 participants