stories icon indicating copy to clipboard operation
stories copied to clipboard

Story should return its execution result.

Open proofit404 opened this issue 3 years ago • 1 comments

Syntax

~~Once again we would change required syntax to work :sweat_smile:~~

from dataclasses import dataclass
from types import SimpleNamespace
from typing import Callable

from stories import story

from app.repositories import load_order, load_customer, create_payment


@story
def purchase(it, state):
    it.find_order(state)
    it.find_customer(state)
    it.check_balance(state)
    it.persist_payment(state)


@dataclass
class Steps:
    load_order: Callable
    load_customer: Callable
    create_payment: Callable

    def find_order(self, state):
        state.order = self.load_order(state.order_id)

    def find_customer(self, state):
        state.customer = self.load_customer(state.customer_id)

    def check_balance(self, state):
        if not state.order.affordable_for(state.customer):
            raise Exception

    def persist_payment(self, state):
        state.payment = self.create_payment(
            order_id=state.order_id, customer_id=state.customer_id
        )


steps = Steps(
    load_order=load_order,
    load_customer=load_customer,
    create_payment=create_payment,
)


state = SimpleNamespace(order_id=1, customer_id=1)


print(purchase.bind(steps).run(state))
purchase
  find_order
  find_customer
  check_balance
  persist_payment


order_id = 1                                                       # Story argument
customer_id = 1                                                    # Story argument
order = Order(product=Product(name='Books'), cost=Cost(amount=7))  # Set by purchase.Steps.find_order 
customer = Customer(balance=8)                                     # Set by purchase.Steps.find_customer
payment = Payment(due_date='tomorrow')                             # Set by purchase.Steps.find_subscription

Result object is...

a restored context representation from stories version 4.0.

Principles

  • Actors: this feature become available just by changing name of the definition from it to something like bot, developer, admin etc.
  • Conditionals: this feature become available just by using if, else and match keywords. Access to the state attribute outside of step would be treated as conditional in result representation. It's value still limited to true, false and enum attribute. it delegate check that step argument is the same as story argument. This pattern would prevent usage of loop inside story definition.
  • Typing: stories.typing would define bunch of generics and protocols for step, steps, state, etc. So it would be possible to annotate story function with __annotations__ future enabled including inner stories.
  • Async: run method should not care if story is a function, a generator, or a coroutine. This kind of API do not need an executor in the first place. run method usage should be transparent to the caller code (a Django view for example). But we would explicitly test and document every feature with sync and async variants.
  • Bound story representation: repr(purchase) and repr(purchase.bind()) would work the same way as it works in stories 4.0.
  • Inner stories: TODO
  • Ecosystem integration: we pass result object to the logger.debug manually in every place. This is boilerplace code we would write inside every Django view. This logger could be an integration layer for pytest, sentry, django logging, ELK, etc. We would document how to use it with most important cases.
  • Replace logger: In some cases users would like to stream business process events as separate log entries using struct log or celery task progress features. purchase.use(struct_log_adapter).bind().run() to stream every step and assignment to ELK using structlog adapter. This adapter return None, so user readable representation is not printed in ipython.

2023 Aug 8 update

Typing support for conditional inner stories without when and unless.

from attrs import define
from stories import Story, I


@define
class Outer(Story):
    I.conditional

    def conditional(self, state):
        if state.condition:
            self.inner(state)

    inner: Story

proofit404 avatar Nov 05 '22 17:11 proofit404