podhmo / apikit

api toolkit (WIP)
MIT License
0 stars 0 forks source link

web, fat API support #158

Closed podhmo closed 2 years ago

podhmo commented 2 years ago

For example, realworld example's ArticleCreate() is fat API https://github.com/gothinkster/golang-gin-realworld-example-app/blob/e4174a9d6450e9e993d9b89a0dace58b7994c045/articles/routers.go#L32-L46

this API's openAPI doc is here https://github.com/gothinkster/realworld/blob/f07b0eff21501d020abc369090c9a18b63747eea/api/openapi.yml#L315-L343

podhmo commented 2 years ago

fat API example

In gothinkster/golang-gin-realworld-example-app, handling this mismatch with Validator and Serializer. Simplified definition is here

func ArticleCreate(c *gin.Context) {
        // input -> model
    articleModelValidator := NewArticleModelValidator()
    if err := articleModelValidator.Bind(c); err != nil {
        c.JSON(http.StatusUnprocessableEntity, common.NewValidatorError(err))
        return
    }

       // model.Do()
    if err := SaveOne(&articleModelValidator.articleModel); err != nil {
        c.JSON(http.StatusUnprocessableEntity, common.NewError("database", err))
        return
    }

       // model -> output
    serializer := ArticleSerializer{c, articleModelValidator.articleModel}
    c.JSON(http.StatusCreated, gin.H{"article": serializer.Response()})
}

definitions .

type ArticleCreateValidator struct {
    // has some fields definitions

    ArticleModel *ArticleModel
}

func (v *ArticleCreateValidator) Bind(c *gin.Context) error {
    // validation and set data to model.
    // s.ArticleModel.<something> = <something>
    return nil
}

type ArticleCreateSerializer struct {
    C *gin.Context
    ArticleModel
}

func (s *ArticleCreateSerializer) Response() ArticleCreateResponse {
    // serialize model to presentation model (response)
    return ArticleCreateResponse{}
}

type ArticleCreateResponse struct {
    // ...
}
podhmo commented 2 years ago

And path parameter is existed, handling 404. so simplified version is here.

// POST /parent/<parent id>/child (e.g. /articles/<article id>/comments)
func CreateParentChild(c *gin.Context) {
    var parent Parent
    parentID := c.Param("parentId")

    // 404 check from path parameter
    if err := FindParent(parentID, &parent); err != nil {
        c.JSON(404, common.NewNotFoundError(err))
        return
    }

    // input -> model
    var model Child
    if err := BindChild(parent, &model); err != nil {
        c.JSON(422, common.NewValidationError(err))
        return
    }

    // models's action
    var result Result
    if err := model.Do(&result); err != nil {
        c.JSON(500, common.NewError("DB", err))
        return
    }

    // model -> output
    var output Output
    if err := SerializeResult(result, &output); err != nil {
        c.JSON(500, common.NewError("serialize", err))
        return
    }
    c.JSON(200, output)
}
podhmo commented 2 years ago

in serializer

And, in golang-gin-realworld-example-app, using global variable for handling DB state. So, this model is application model that can access DB in internal method of this one. And in serializer, transform id to data and inject loginUser info from gin's Context.

# with DB data (internal data)
{xId: 10} -> {x: {id: 10, name: "..", ...}}

# with request data (external data)
{data: ..} -> {author: {name: "login user", ...}, data: ...}
podhmo commented 2 years ago

slim API example

RPC

func CreateArticle(db *DB, input Input) (*Output, error) {
    return db.Save(input)
}

currently support this version only.

podhmo commented 2 years ago

Simply, define action and web-action with transform data.

slim API

handler -> action

fat API

handler -> web-action -> action

But integrate web-action to action is maybe tiresome. (If many many dependencies are existed) using global variable is the one of solutions for this situation (and if so, can skip to use apikit, maybe, but, but ...)

podhmo commented 2 years ago

This is nonsense

// How to handle 404?

r.Post("/articles/{articleId}/comments",
 CreateArticleComment,
 WithBinder(BindArticleComment),
 WithSeiralizer(SerializeCreateArticleComment),
)
podhmo commented 2 years ago

In a normal DI system, all components (serializers, binders, etc.) are injected using that system, but in apikit, the components are processed in a hand-crafted interface (for example, the provider interface has a GetDB method) to do. That is why, this kind of action is not realistic.

podhmo commented 2 years ago

Why can't a wire-like feature solve it?

And how do we handle cases with many more other dependencies? (For example, sending emails, sending messages to queues, notifications, monitoring, etc.)

podhmo commented 2 years ago

https://github.com/podhmo/apikit/issues/158#issuecomment-962083461

Currently, this is best.