Open sobolevn opened 5 years ago
Or maybe predefine all Success
cases as well?
This could possibly look like so:
@dataclass
class Post(object):
title: str
content: str
class SuccessCases(ResponseEnum): # typed abstract subclass of enum.Enum
created = inline_factory(Post, 201, {'Location': 'What to do with the value!?'})
untouched = SomeDefinitionWIthClass
class ErrorCases(ResponseEnum):
not_found = inline_error_factory(404, {})
Still don't like this part with headers and default values for it. Not all headers can have a default value. Not all headers should. But, providing declarative headers with values seems like a hard task to me.
Why not lazy templates? Maybe we can go with something like {'Location': '/posts/{{ instance.pk }}'}
. And then just format the template with instance
and request
context?
Maybe it is unrelated. This is the current state of the @dry-python project.
# views.py
from dependencies import Injector, Package, operation
from dependencies.contrib.rest_framework import model_view_set
implemented = Package("implemented")
@model_view_set
class Products(Injector):
edit = implemented.EditProduct.edit
@operation
def update(edit, instance, validated_data, user, invalid_request, permission_denied):
result = edit.run(product=instance, edits=validated_data, user=user)
if result.is_success:
return result.value
elif result.failed_because(edit.failures.invalid_input):
invalid_request()
elif result.failed_because(edit.failures.forbidden):
permission_denied()
# implemented.py
from dependencies import Injector, Package
services = Package("services")
repositories = Package("repositories")
class EditProduct(Injector):
edit = services.EditProduct.edit
update_product_record = repositories.update_model_instance
# services.py
from dataclasses import dataclass
from enum import Enum, auto
from typing import Callable, Dict
from django.db.models import Model
from stories import Failure, Result, Success, arguments, story
@dataclass
class EditProduct:
@story
@arguments("product", "edits", "user")
def edit(I):
I.validate_input
I.check_permissions
I.persist_edits
I.show_product
# Steps.
def validate_input(self, ctx):
if False:
return Failure(Errors.invalid_input)
else:
return Success()
def check_permissions(self, ctx):
if False:
return Failure(Errors.forbidden)
else:
return Success()
def persist_edits(self, ctx):
product = self.update_product_record(ctx.product, ctx.edits)
return Success(updated_product=product)
def show_product(self, ctx):
return Result(ctx.updated_product)
# Dependencies.
update_product_record: Callable[[Model, Dict], Model]
@EditProduct.edit.failures
class Errors(Enum):
invalid_input = auto()
forbidden = auto()
# repositories.py
def update_model_instance(instance, data):
for attr, value in data.items():
setattr(instance, attr, value)
instance.save(update_fields=data)
As you can see, we define failure protocol for the story.
Then in the view layer we check what kind of failure business object returns.
Based on business logic error we decide what API method we should call.
This manual check with lots of elif
make me sad very often...
It doesn't validate that all failure variants were covered.
I think to add something like this with stories bind
method.
# views.py
from dependencies import Injector, Package, this
from dependencies.contrib.rest_framework import model_view_set
implemented = Package("implemented")
@model_view_set
class Products(Injector):
update = implemented.EditProduct.edit.bind
product = this.instance
edits = this.validated_data
user = this.user
on_invalid_input = this.invalid_request
on_forbidden = this.permission_denied
@operation
def update(edit, instance, validated_data, user, invalid_request, permission_denied):
result = edit.run(product=instance, edits=validated_data, user=user)
if result.is_success:
return result.value
elif result.failed_because(edit.failures.invalid_input):
invalid_request()
elif result.failed_because(edit.failures.forbidden):
permission_denied()
Sorry, did get what invalid_request
and permission_denied
are in this context.
This ara shortcuts for common actions in django rest framework. They came from @model_view_set
. You can define your own transport actions in the same Injector
class next to the edit
attribute.
Previously I had an idea to use attribute
controller: Callable[[RequestPayload], ResponsePayload]
, but I have failed to come up with correct way to handle headers and status codes. And this feature is crucial.So, I had spent some time thinking about it. Here's a quick recap:
Request
andResponse
classes inside a business logic controller. This will make my business logic dirty and will enforce bad practices of mixin my representation, interface layer, and the logic itself.Enum
? It is declarative, typed, easy yo use. I don't see any reasons why we should not do that.