asyncapi / optimizer

AsyncAPI offers many different ways to reuse certain parts of the document like messages or schemas definitions or references to external files, not to even mention the traits. There is a need for a tool that can be plugged into any workflows and optimize documents that are generated from code, but not only.
Apache License 2.0
14 stars 9 forks source link

docs: moving forward with this library #4

Closed KhudaDad414 closed 3 years ago

KhudaDad414 commented 3 years ago

Reason/Context

Fortunately, the project is initialized and we can discuss the API of this library here.

Description

Here is a draft of the API as well as the proposed Report structure. After finalizing the API and report structure, I will open a pull request with these changes in the README.md file.

Usage

Node.js

import { Optimizer } from '@asyncapi/optimizer';
import { parse } from '@asyncapi/parser';
let asyncApiDocument = parse(`
asyncapi: 2.0.0
info:
  title: Streetlights API
  version: '1.0.0'

channels:

  smartylighting/event/{streetlightId}/lighting/measured:
    parameters:
      #this parameter is duplicated. it can be moved to components and ref-ed from here.
      streetlightId:
        schema:
          type: string
    subscribe:
      operationId: receiveLightMeasurement
      traits:
        - bindings:
            kafka:
              clientId: my-app-id
      message:
        name: lightMeasured
        title: Light measured
        contentType: application/json
        traits:
          - headers:
              type: object
              properties:
                my-app-header:
                  type: integer
                  minimum: 0
                  maximum: 100
        payload:
          type: object
          properties:
            lumens:
              type: integer
              minimum: 0
            #full form is used, we can ref it to: #/components/schemas/sentAt
            sentAt:
              type: string
              format: date-time

  smartylighting/action/{streetlightId}/turn/on:
    parameters:
      streetlightId:
        schema:
          type: string
    publish:
      operationId: turnOn
      traits:
        - bindings:
            kafka:
              clientId: my-app-id
      message:
        name: turnOnOff
        title: Turn on/off
        traits:
          - headers:
              type: object
              properties:
                my-app-header:
                  type: integer
                  minimum: 0
                  maximum: 100
        payload:
          type: object
          properties:
            sentAt:
              $ref: "#/components/schemas/sentAt"

components:
  messages:
    #libarary should be able to find and delete this message because it is not used anywhere.
    unusedMessage:
      name: unusedMessage
      title: This message is not used in any channel.

  schemas:
    #this schema is ref-ed in one channel and used full form in another. the library should be able to identify and ref the second channel as well.
    sentAt:
      type: string
      format: date-time`
);
let optimizer = new Optimizer(asyncApiDocument);
let report: OptimizerReport = await optimizer.getReport();
/*
the `report` value will be:
{
  ReuseComponents: [
    {
      path: '#/channels/channel1/smartylighting/event/{streetlightId}/lighting/measured/message/payload/properties/sentAt',
      action: 'ref',
      refTo: '#/components/schemas/sentAt'
    }
  ],
  RemoveComponents: [
    {
      path: '#/components/messages/unusedMessage',
      action: 'remove',
    }
  ],
  MoveToComponents: [
    {
      //move will ref the current path to the moved component as well.
      path: '#/channels/smartylighting/event/{streetlightId}/lighting/measured/parameters/streetlightId',
      action: 'move',
      moveTo: '#/components/parameters/streetlightId'
    },
    {
      path: '#/channels/smartylighting/action/{streetlightId}/turn/on/parameters/streetlightId',
      action: 'ref',
      refTo: '#/components/parameters/streetlightId'
    }
  ]
}
 */
let optimizedDocument = optimizer.getOptimizedDocument({rules: {ReuseComponents: true,RemoveComponents: true,MoveToComponents: true }})
/*
the `optimizedDocument` value will be:

asyncapi: 2.0.0
info:
  title: Streetlights API
  version: '1.0.0'

channels:

  smartylighting/event/{streetlightId}/lighting/measured:
    parameters:
      streetlightId:
        $ref: "#/components/schemas/streetlightId"
    subscribe:
      operationId: receiveLightMeasurement
      traits:
        - bindings:
            kafka:
              clientId: my-app-id
      message:
        name: lightMeasured
        title: Light measured
        contentType: application/json
        traits:
          - headers:
              type: object
              properties:
                my-app-header:
                  type: integer
                  minimum: 0
                  maximum: 100
        payload:
          type: object
          properties:
            lumens:
              type: integer
              minimum: 0
            #full form is used, we can ref it to: #/components/schemas/sentAt
            sentAt:
              $ref: "#/components/schemas/sentAt"

  smartylighting/action/{streetlightId}/turn/on:
    parameters:
      streetlightId:
        $ref: "#/components/schemas/streetlightId"
    publish:
      operationId: turnOn
      traits:
        - bindings:
            kafka:
              clientId: my-app-id
      message:
        name: turnOnOff
        title: Turn on/off
        traits:
          - headers:
              type: object
              properties:
                my-app-header:
                  type: integer
                  minimum: 0
                  maximum: 100
        payload:
          type: object
          properties:
            sentAt:
              $ref: "#/components/schemas/sentAt"

components:
  parameters:
    streetlightId:
      schema:
      type: string
  schemas:
    #this schema is ref-ed in one channel and used full form in another. library should be able to identify and ref the second channel as well.
    sentAt:
      type: string
      format: date-time`
 */

API

Constructor

new Optimizer(document)

document is a mandatory object that is parsed with @asyncapi/parser:

Methods

getReport() : OptimizerReport

getOptimizedDocument([options]) : string

github-actions[bot] commented 3 years ago

Welcome to AsyncAPI. Thanks a lot for reporting your first issue. Please check out our contributors guide and the instructions about a basic recommended setup useful for opening a pull request.

Keep in mind there are also other channels you can use to interact with AsyncAPI community. For more details check out this issue.

KhudaDad414 commented 3 years ago

@derberg @jazzyarchitects @magicmatatjahu @ Any input would be appreciated especially with report object structure.

magicmatatjahu commented 3 years ago

@KhudaDad414 Great work! Some my thoughts:

      path: '#/channels/channel1/smartylighting/event/{streetlightId}/lighting/measured/message/payload/properties/sentAt',
      action: 'ref',
      refTo: '#/components/schemas/sentAt'

but I have two problems:

derberg commented 3 years ago

awesome stuff:

magicmatatjahu commented 3 years ago

I agree with Łukasz, these two last aren't need, we can make issues and see if someone from community will want this features :)

jazzyarchitects commented 3 years ago

Nice report structure @KhudaDad414. In addition to the consistency part mentioned in the previous comments, IMO it would be more awesome if the objects in each of the three arrays have the same structure. One of the objects have moveTo field and one has refTo. While this might not be a deal breaking thing, from what I have seen, having a single structure format helps when the output is consumed or parsed by some strictly typed languages.

Secondly, would it be a better parsing experience from a consumers point to have only a single array and segregate based on different actions (@derberg , @magicmatatjahu would like to hear your thoughts on this)?

jazzyarchitects commented 3 years ago

Another suggestion, how about a chained interface?

const changes =  await optimizer.getReport(); // Which can return an object of say `Report` class
changes.apply({rules: })
KhudaDad414 commented 3 years ago

it would be more awesome if the objects in each of the three arrays have the same structure. One of the objects have moveTo field and one has refTo

as you suggested, for consistency it would be a good idea to change moveTo and refTo to another name like target. In that case, if we have another type of optimization in the future, it can use path, action, target structure.

magicmatatjahu commented 3 years ago

Secondly, would it be a better parsing experience from a consumers point to have only a single array and segregate based on different actions.

@jazzyarchitects It's a some idea, but there is a one problem: if someone will operate on this array, will have to check on every item the kind of operation - move to, ref to etc. even if array will be segregated, so object with reuse..., remove... move... etc is a better shape for me, but of course I don't think so that it will be hard to change it in the future, so we can discuss about it :)

Another suggestion, how about a chained interface?

There is a some solution. I have no opinion on this, because both the chained functions and normal functions will be good here.

as you suggested, for consistency it would be a good idea to change moveTo and refTo to another name like target. In that case, if we have another type of optimization in the future, it can use path, action, target structure.

@KhudaDad414 Please, don't forget about document (of course in the next PRs) the shape of object in the docs/Readme.md, especially target :)

derberg commented 3 years ago

How about:

{
  ReuseComponents: [
    {
      path: '#/channels/channel1/smartylighting/event/{streetlightId}/lighting/measured/message/payload/properties/sentAt',
      refTo: '#/components/schemas/sentAt'
    }
  ],
  RemoveComponents: [
    {
      path: '#/components/messages/unusedMessage',
      remove: true
    }
  ],
  MoveToComponents: [
    {
      path: '#/channels/smartylighting/event/{streetlightId}/lighting/measured/parameters/streetlightId',
      moveTo: '#/components/parameters/streetlightId'
    },
    {
      path: '#/channels/smartylighting/action/{streetlightId}/turn/on/parameters/streetlightId',
      refTo: '#/components/parameters/streetlightId'
    }
  ]
}

I just had a thought: what's the point of having action if I can figure it out through the key name

anyway, I think the best way is to think about it from the point of view of the user that will read the report in the UI and the JS code that will parse the report. In the end, you will not print the Map to the user, right?

as a user, I want to:

now, is it better if it will be no action or there will be action, or there will be ref or reuse? you will figure it out the best during integrating the library in CLI or UI.

Conclusion: I suggest pick the best structure you think based on our feedback, implement, and then integrate and make improvements that will make your integration code nicer 😄 The good thing is that it is a library in the development phase, the structure of the report may change frequently, so better I think is to pick something that you think should be good and then iterate improvements.

Thoughts?