mistyharsh / marble-mix

Common Utilities for Marble.js
https://github.com/marblejs/marble
MIT License
9 stars 0 forks source link

Opinion on new response toolkit for Marble.js #3

Open mistyharsh opened 5 years ago

mistyharsh commented 5 years ago

Hi all,

Currently, we are in a process of building a response helpers/toolkit for Marble.js. Effect object expects { status?, body?, headers? }: EffectResponse as the standard object. However, it is often clumsy to work with headers when headers like Content-Type, Location or Cookie are involved.

Thus, we are building a fluent-like yet functional interface to simply generating EffectResponse object. I have basically two things in mind:

// mr stands for marble response
import { mr } from 'marble-mix';

// Example with eager syntax
const effect$ = EffectFactory
    .matchPath('/some-path')
    .matchType('GET')
    .use((req$) => req$.pipe(

        switchMap((req) => {

            return mr.make(200).pipe(

                // Override status code
                mr.code(400),

                // Set body: object or string
                mr.body('Some body'),

                // Add new header or override/append existing header
                mr.header('key', 'value', { /* Header options: append/override */}),

                // Set cookie with key `key` and value `value`
                mr.state('key', 'value', { /* Cookie options: ttl, expiry, domain, etc. */}),

                // Delete cookie with key `cookie-key`
                mr.unstate('cookie-key')
            );
        })
    ));

In the above example every function has uniform return type: Observable<EffectResponse>. Every fuction like code, body, etc. is basically a custom Rx.js operator. Every operator takes existing incoming response object, creates new copy after making changes and returns new EffectResponse. There are few more functions like created, location, type, etc. However, the problem with this syntax is cookies. Cookies are bit difficult to process as I might need to parse existing Set-Cookie header in this pipeline. In essence, these functions are simply manipulating EffectResponse and producing new EffectResponse.

Second option is maintaining a custom response object. Instead of manipulating EffectResponse, they will manipulate a custom internal object. One the user is done with the pipeline, he will have to invoke build() function which will produce Observable<EffectResponse>. Here, all the computation is differed till the last build() function. It will help enable do certain optimizations especially if cookies are encrypted and involves time consuming operations. In will look like:

.use((req$) => req$.pipe(

    switchMap((req) => {

        return mr.make(200).pipe(

            // Override status code
            mr.code(400),

            // Set body: object or string
            mr.body('Some body'),

            // Add new header or override/append existing header
            mr.header('key', 'value', { /* Header options: append/override */}),

            // Set cookie with key `key` and value `value`
            mr.state('key', 'value', { /* Cookie options: ttl, expiry, domain, etc. */}),

            // Delete cookie with key `cookie-key`
            mr.unstate('cookie-key')
        );
    }),

    // IMPORTANT STEP
    switchMap((x: CustomResponse) => mr.build(x))
));

In above step, mr.build(x) finally produces Observable<EffectResposne>. Few design decisions we have made here like:

  1. No OOP style fluent interface
  2. Everything is a function
  3. Reuse Rx.js pipeline as it is.

Keeping it compatible with pipeline so that in future we can have much simpler interface:

   return mr.make(200)
   |> code(400)
   |> body('Great interface')
   |> header('key', 'value', options)

@JozefFlakus @sagar735 @Krantisinh @minitesh any opinions with the syntax?

JozefFlakus commented 5 years ago

@mistyharsh I like the idea, especially with the idea of pipeline operator compatibility. I would really like to see this proposal soon 😋

Currently I'm building the similar response creator inside new websockets package and right now our ideas are very similar. Regarding your concept, I was thinking about dedicated rx-operator which will expose some builder as a second argument. Why an operator? Because it is more explicit.

const effect$ = req$ =>
  req$.pipe(
    // ...
    mapToResponse((input, b) => b.status(200).pipe(
        b.status(400),
        b.body(input),
        b.header('key', 'value', options)
        // ...
      )
    ),
  );

In the above example, the mapToResponse will deliver a piped input value from previous operator and builder/creator/factory/whaeva which then can be used for building an EffectResponse object.

BTW. I would really love to see this kind of operator in @marblejs/core package! 💪

mistyharsh commented 5 years ago

Thank you @JozefFlakus for the inputs. The idea of having a dedicated builder interface is a great. I will experiment with this and see where it leads us.