asyncapi / asyncapi-react

React component for rendering documentation from your specification in real-time in the browser. It also provides a WebComponent and bundle for Angular and Vue
https://asyncapi.github.io/asyncapi-react/
Apache License 2.0
174 stars 106 forks source link

Proposal to define Modules aka Plugins for UI component and Studio (Editor) #433

Closed magicmatatjahu closed 11 months ago

magicmatatjahu commented 2 years ago

Proposal to define Modules aka Plugins for UI component and Studio

Abstract

Currently, every part of our component is hardcoded, i.e. users cannot change the rendering of a given element, wrap it and add e.g. add additional HTML elements in rendering plugins or completely change it or omit it from rendering. At the moment, the only option is to specify whether a section, e.g. Schemas, should be rendered or not by passing the appropriate configuration as a config prop to the component:

import AsyncApiComponent, { ConfigInterface } from "asyncapi-react";

const config: Partial<ConfigInterface> = {
  schemaID: 'custom-name',
  show: {
    schemas: false
  },
};

const DOC = <AsyncApiComponent schema={...} config={config} />;

Another problem is the inability to interact with the internal state of the component. For example, if someone provides bad AsyncAPI documentation and the parser throws an error, then this error is rendered by the component, but the user himself/herself has no way to catch this error. The solution would be to make state available via hooks as described in this issues (https://github.com/asyncapi/asyncapi-react/issues/305) like:

<AsyncAPIComponent 
  schema={...} 
  config={...}
  hooks={{
    onErrorParse: (...some props including error) => { ...logic },
    onSuccessParse: () => { ...logic },
    ...etc
  }}
/>

But the solution itself is not future-proof. If some state is not made available then the user will be powerless and will have to make contribution. The user should connect to the state from the inside (through a plugin) and not from the outside. And to be clear: I don't have problem with contributions but I have a big problem with complexity of props in React component and their "low usefulness" from outside.

I have to mention that this is not a requirement, but rather something that should be available - overriding basic functionality in the component, e.g. parser logic.

So now we have two (three) problems - hardcoded components and no interference with the internal state of the component and its functionality.

Module aka Plugin concept

Introduction

If you made it through the above abstract, I invite you to something more abstracted - Modules aka Plugins :smile:

A Module (Plugin) is a package of functionalities that can be used in the system. This can be compared to modules in JS (import, export), but the difference is that a Module only contains definitions of how to create something, not that something is already available and created (has value). In JS modules you can create something like

export const SOME_VALUE = "Some string"`

and it's exactly value that you can consume in another JS modules, however it's harcoded, you cannot change representation of it in runtime (in some cases you can, but if it is const or not exported you cannot) - in Module everything is a "factory" that you can wrap, or even intercept the value returned by the factory and change it to something else altogether. It's still important that the author of the module must know what he's/she's doing, it means that if person wants to change a global value which is an AsyncAPI parsed document, the same document must be returned (and only extended with e.g. extensions) - if changes the value of the document to e.g. a number then the whole component may stop properly working.

Providers

As mentioned, each element of the module is a "factory". The name of this element is Provider. It shape is:

{
  provide: {TOKEN}
  {KIND_OF_FACTORY}: {FACTORY}
  when: {WHEN}
  {...specific fields for KIND_OF_FACTORY}
}

where:

Worth to mention is that useFactory and useClass can use another values from system (before creation) by inject field, when you define tokens from system:

{
  provide: "SOME_TOKEN"
  useFactory: (asyncAPIDoc, anotherToken) => { ...logic }
  inject: [AsyncAPIDocument, SOME_TOKEN] // inject parsed AsyncAPIDocument as first argument to function etc...  
}

IMPORTANT: I meant in the phrase Modules aka Plugins in the title of proposal, that modules are such plugins that add additional factories to system, not define some contract for some part of application, but it's still possible. So modules are not classic plugins that have a specific interface to implement.

Components as providers

Okay, but as reader you're probably asking how this relates to components, because so far you only see using the system to add state/functionality to the application, not to add components that can be used?

Components (their definitions) can also be used as providers in the system:

const SomeReactComponent = (props) => {
  return ....
}

// providers
{
  provide: "SomeReactComponent",
  useValue: SomeReactComponent, // assign to the `SomeReactComponent` token reference to the SomeReactComponent React component
}

const ComponentWhichUsesSystem = (props) => {
  const parsedDoc = useInject(AsyncAPIDocument); // use parsed AsyncAPI Doc
  const SomeReactComponent = useComponent('SomeReactComponent'); // retrieve from system the SomeReactComponent component which is saved as `SomeReactComponent` token in the system

  return (
    <SomeReactComponent {...} />
  );
}

useInject is a custom React hooks which returns a given value from the system assigned to the token. useComponent is a custom React hooks which has reference to the system and returns from it the reference to the React component. This is a sugar syntax of useInject to retrieve components.

Wait, wait, wait... isn't this the DI system known from Angular2+?

https://i.kym-cdn.com/photos/images/newsfeed/001/650/747/aaf.png

...but has more possibilities.

Contextual injection

Why we need when field in provider definition? when indicates when the value should be created and when it should be injected - at some point in the application that meets certain conditions. In the case of the AsyncAPI component, such a thing will mainly useful for the rendering of custom bindings and for components that work only with appropriate version of AsyncAPI:

Of course when will work also for normal values and factories (which are not components):

  // providers
  {
    provide: "SOME_TOKEN",
    useFactory: () => {...},
    when: (...params) => {...}
  }

Module representation in React VDOM

The module is actually a simple React component that creates context underneath and manages the passed providers (factories) and their values:

const App = () => {
  return (
    <Module module={...providers}>
      <Component />
    </Module>
  );
}

...and then ComponentWhichUsesSystem component can use the values from Module by useInject and useComponent.

Additionally, if there are no tokens in the nearest Module, then the system searches the parent until it finds a token or the parent doesn't exist. so in the example:

const App = () => {
  return (
    <Module module={...}> // Module#1 has `Binding` component
      ...
        ...
          <Module module={...providers}> // Module#2 hasn't `Binding` component
            <Component /> // component which want to use the `Binding` component - will be injected from Module#1
          </Module>
        ...
      ...
    </Module>
  );
}

Importing modules

What would a plugin system be without the ability to import other plugins? In addition to the providers, the module also has two fields, imports and exports. The first one imports the custom modules to the given one. The second one exports the providers to the "parent" module, where module is imported.

// ImportedModule
{
  providers: [
    ...provider with "SOME_TOKEN" token,
    ...provider with "ANOTHER_TOKEN" token
  ]
  exports: ["SOME_TOKEN"]
}

// AppModule
{
  imports: [ImportedModule] // imports only `SOME_TOKEN` token
  providers: [...]
}

Other possibilities

There are an other ideas and possibilities that I don't want to write about because this is not a book but a proposal, but I will list some of them:

FAQ

Feedback

I know that some people don't like DI on the front-end, but I don't see any other approach that would give us so many possibilities, along with wrapping existing components and not overwriting them. I also have a few problems on the idea itself, because some things may seem too abstract (and hard to understand in first look), but I already simplify a few things in my proposal, trust me, it was really shit. In addition, if we go in this direction, it will have to be very well documented, but having @alequetzalli in team I think it will not be a problem :smile:

I've been working on this problem for over 3 months, since mid-June and this proposal is the culmination of that. Let me know what you think and if you've read to this point, thanks for reading! If the suggestion sucks, I'd like to hear another, because personally I don't want to destroy our component by some shitty idea.

BOLT04 commented 2 years ago

This proposal is very well structured and has a lot of information πŸ‘. I'm sure it was challenging coming up with this summary after 3 months of work πŸ˜…

I'd like to share some thoughts on some of the points you mentioned πŸ™‚.

I'm having doubts about exposing the whole internal state of the component, mostly because it seems we would be exposing implementation details. In a plugin architecture I see the use of an interface as very beneficial, in order to provide a contract for the user to follow: plugin dependency flow interface Photo taken from: https://www.thisdot.co/blog/plugin-architecture-for-angular-libraries-using-dependency-injection

I could be just misinterpreting your words πŸ˜…, so the plan might not be to expose implementation details, but instead to provide an interface for plugin authors.

Also, I agree with using DI in this scenario πŸ‘. I am more curious about the Plugin contracts and how the user knows about it. Have you thought about what these could be? In terms of the contract definition, it would be simply a TypeScript interface that the core npm module exposes.

Implementation scenario There were a few items I haven't completely understood how they work, so I'd like to experiment with a scenario to put things in "practice". Let's take the error handling scenario, how would it look like with this Modules aka Plugins system?

I'm thinking the AsyncApiComponent would have to import this useInject() hook to access the "IoC container" and pass in 'errorPlugin' for example for the {TOKEN}. This would return a list of functions that we would then call passing the error object as an argument. I'm saying a list of functions because there could be multiple plugins hooked into the errorPlugin token. This allows composing plugins on top of each other, each implementing its logic.

Here is a code snippet of what it would like here:

const { validatedSchema, error } = this.state;
//...
const errorPlugins = useInject('errorPlugin')
const errorPluginComponents = errorPlugins.map(pluginFunc => pluginFunc(error)).filter(pluginComponent => pluginComponent)

return concatenatedConfig.showErrors && <>
  <ErrorComponent error={error} />
  errorPluginComponents && errorPluginComponents.map(ErrorPluginComponent => <ErrorPluginComponent />)
</>;

Somewhere in the core package, we would have this type definition:

interface ErrorPlugin {
   message: string
   stackTrace: string
}

Then this would be the plugin package:

const providers = [
  {
    provide: 'errorPlugin',
    useFactory: (error: ErrorPlugin) => {
      // use a 3rd party package to display an error notification
      return ErrorToaster; // This is a function that represents a React component. In this example we don't return <ErrorToaster /> because it's the core package that instantiates the React component πŸ™‚ 
    }
  }
]

<Module module={...providers}>
  <AsyncAPIComponent />
</Module>

In this example, the function we defined in useFactory should be called with the error object, and then display the ErrorToaster component on the page.

@magicmatatjahu can you share your thoughts on this implementation scenario and how you think it would like? I'm focusing a lot on the user's perspective to see how we could build on top of the Modules system πŸ˜ƒ

magicmatatjahu commented 2 years ago

@BOLT04 Wow! Thanks for this comment, this is what I was hoping for, because you ask specific questions and I love it! :)

I will try to answer all your questions

I'm having doubts about exposing the whole internal state of the component, mostly because it seems we would be exposing implementation details. In a plugin architecture I see the use of an interface as very beneficial, in order to provide a contract for the user to follow:

I had this doubts too, whether to share or not, but I remember one particular comment in one issue (https://github.com/asyncapi/asyncapi-react/issues/394#issuecomment-881581424) As you can see, by sharing the state of a component we can give the ability for users to read it so that it is only "readonly". Of course, there is still a possibility that an outside user will start to interfere with the state, but this is a side effect - if starts to do it, user has to take into account that the component will stop properly working or will be broken.

I've also decided to make the component state available to the outside for a simple reason - it will be useful for us (as maintainers) in the Studio in several places. A simple example that will be implemented in the future:

If you go to [the link]https://app.swaggerhub.com/apis/fatimahnasser99/TopInternsApplicantsAPI/1.0.1 and click on that left arrow icon, you are automatically taken to the appropriate line in the editor :)

image

That's the kind of functionality I'm looking for in the future. This shared state will help us in this :)

Regarding this aspect of Plugins, here my mistake for not explaining it in the introduction. I meant in the phrase Modules aka Plugins, that modules are such plugins that add additional factories to the dependency injection system, so Modules are Plugins for the IOC container itself :) I don't know what is your knowledge of DI in Angular2+, but it seems to me that you know Angular2+ or not? If so, you should know exactly what I'm talking about, the module extends collections which you can reuse in your system. Why did I use that name? Modules would probably not tell people anything, and in fact a module is a plugin, but not for a contract (as usual), but for dependencies (like in Angular).

I could be just misinterpreting your words πŸ˜…, so the plan might not be to expose implementation details, but instead to provide an interface for plugin authors.

Also, I agree with using DI in this scenario πŸ‘. I am more curious about the Plugin contracts and how the user knows about it. Have you thought about what these could be? In terms of the contract definition, it would be simply a TypeScript interface that the core npm module exposes.

Yes and no :smile: On one hand we will provide this implementation details, but I would call it more the internal state of the component, but on the other hand it will be possible to define contracts for given parts of the component, like in old-school plugins. When it comes to this contract or rather its TS type I mentioned something like InjectionToken in the proposal. And this is its advantage, that it can take a type that must be returned by the factory. So let me use your example with these error plugins:

type PLUGIN_TYPE = React.JSXElementConstructor<React.PropsWithChildren<{ error: Error }>>; // React... types are exactly TS types for React component with { error: Error } props
const ERROR_PLUGIN = new InjectionToken<PLUGIN_TYPE>('ERROR_PLUGIN') // 'ERROR_PLUGIN' argument will be the name of token and will be used mainly for debug purposes :)

const ErrorToaster: React.FunctionComponent<{ error: Error }> = ({ // check this line - it means that `error` prop for `ErrorToaster` component is required, so we exactly implement above contract for PLUGIN_TYPE
  error,
}) => {
  ... logic
}

// our provdiers
const providers = [
  {
    provide: ERROR_PLUGIN,
    useValue: ErrorComponent, // we treat our internal component `ErrorComponent` like provider
  }
  {
    provide: ERROR_PLUGIN,
    useValue: ErrorToaster, // ErrorToaster implements PLUGIN_TYPE contract
  },
];

// `Multi()` helper, aka wrapper tell us that we want to retrieve all providers saved to the `ERROR_PLUGIN` token
// also errorPlugins are already resolved references to the component so we don't need to call factories, because this factories are called by system, so line
// errorPlugins.map(pluginFunc => pluginFunc(error)) isn't needed
const errorPlugins = useInject(ERROR_PLUGIN, Multi())

return concatenatedConfig.showErrors && <>
  errorPlugins && errorPlugins.map(ErrorPluginComponent => <ErrorPluginComponent error={error} />)
</>;

<Module module={...providers}>
  <AsyncAPIComponent />
</Module>

So as you can see, you have a good understanding of the Modules and the proposed system, I just showed you how to use the contracts for the providers :)

In this example, the function we defined in useFactory should be called with the error object, and then display the ErrorToaster component on the page.

You can still use the useFactory, but for compoments I recommend using useValue more :)

So, the modules themselves do not implement any contract, but you as a user can implement such a contract in your module and import it into our component and will be able to use it it in any place :) That's idea behind modules, and by this I dpn't want to use the Swagger Plugins approach, which has limitation in this case.

Let me use a sentence from the blog post you shared (its image):

The Plugin Architecture pattern is a great pattern to create extensible systems using the Inversion of Control Principle and lifting the focused functionalities of our system to the Plugins.

And the focused is the keyword in our system. With our system you will be able to create a Plugin with a contract, but only if you need to - the system will not force you to do so.

In general, I'm a fan of distributed responsibility, so I'll be pushing the option to make such contracts in our component as little as possible, only where it's needed.

I also read your blog post about AsyncAPI and it is great! You mentioned our roadmap and what we want to achieve - reusing and combining existing specs and tooling, so the module system should also allow us to do that in React component case, and that's what I was thinking about when creating the module proposal. Maybe if the idea works, we will follow the same approach in our other tools? Who knows.

I hope most things have been resolved :) Do you have any other questions/remarks? Thanks again for your comment, as probably my response to it will clear up doubts for other people.

BOLT04 commented 2 years ago

@magicmatatjahu awesome, thank you so much for your comment as well πŸ˜ƒ. I understand the proposal better now, by making the internal state "readonly" we can give users a lot of flexibility πŸ‘. Also, the example you gave for clicking the arrow and going to the specification on that line is pretty cool!

I understand the module contract's approach as well πŸ‘. Basically, we are giving users flexibility and a choice, not enforcing a contract on them πŸ‘.

Maybe if the idea works, we will follow the same approach in our other tools? Who knows.

yeah plugins everywhere πŸŽ‰ πŸŽ‰ πŸ˜„. It's a great pattern to add extension points to the core system, so we might be able to use it on other repos.

I think for now everything is cleared, just one question on how this will proceed when there is more feedback from the community? Will there be a PR implementing the whole system, or divided in multiple issues with a task list, separating the PRs?

magicmatatjahu commented 2 years ago

@BOLT04 No problem and thanks!

I think for now everything is cleared, just one question on how this will proceed when there is more feedback from the community? Will there be a PR implementing the whole system, or divided in multiple issues with a task list, separating the PRs?

I don't know how long we will wait, maybe a week, maybe two. Maybe someone will propose a better idea and this one will not be implemented. In fact, I've already implemented most of the system myself, because I had to see if it sounded as good in code as on paper and if it was possible to implement such a thing at all, and yes, it is.

If we go in this proposal, the system itself should be a separate package, so that you can use it for example in NodeJS without any problem or in next front-end projects, so it may be that 1 PR will add the system (package) to dependencies of this repo, and the next ones will be related to rewriting components to work with the system. How long will it take? Tbh I don't know.

jonaslagoni commented 2 years ago

That sounds like a solid suggestion πŸ‘

I know that some people don't like DI on the front-end, but I don't see any other approach that would give us so many possibilities, along with wrapping existing components and not overwriting them.

I don't see any other approach either, you simply cannot provide such extensibility without such a plugin system to the components, so the suggestion sounds solid as you have clear use-cases that follow why it is needed πŸ‘

It does for sure increase the complexity of the implementation, but it's needed.

magicmatatjahu commented 2 years ago

It does for sure increase the complexity of the implementation, but it's needed.

Sure, the complexity of the modules logic itself won't be easy, but the usage won't be that difficult to understand - I hope πŸ˜…

boyney123 commented 2 years ago

Hey @magicmatatjahu , thanks for the write-up here, I'm still trying to digest things and understand them, but could you give an example of the point of view from a plugin author.

Let's say I wanted to add a new page/widget/plugin to the studio, let's say:

Would this system allow me to do that? Any code examples?


Also another note, you mentioned about getting access to internal state etc, did you consider or look at render callback patterns? I know they are abit "dated" but they do solve these kinda issues, when you want to expose state or the ability to do things within components themself.

Also, another side note, your provider pattern you mentioned above, I quite like that, a lot of libs/frameworks follow the standard provider pattern and pass things into it, like your one for example as the doc passed into it, I'm not sure that's a terrible solution πŸ€”, why don't you like the doc creation outside the provider?

magicmatatjahu commented 2 years ago

@boyney123 Hi! Thanks for comment!

I have a new component that I would like to render on the studio

You can add new component by normal token, or with some predefined token in the studio, which will be used to retrieve all given providers/components (component is also treated as provider) like above:

I use the example based on react-router to create routing

// component.tsx
import React from 'react'
import { render } from 'react-dom'
import { Router, Route, Link } from 'react-router'

type PAGE_TYPE = React.JSXElementConstructor<React.PropsWithChildren<{}>>; // React type
const PAGES = new InjectionToken<PAGE_TYPE>('PAGES') // 'PAGES' argument will be the name of token and will be used mainly for debug purposes :)

// our providers
const providers = [
  {
    provide: PAGES,
    useValue: SingleOrderPage,
    annotations: { // you can also pass some metadata to provider
      path: '/orders/{id}'
    }
  },
  {
    provide: PAGES,
    useValue: OrdersPage, 
    annotations: { // you can also pass some metadata to provider
      path: '/orders'
    }
  },
  {
    provide: 'Layout',
    useValue: DefaultLayout,
  },
];

const DefaultLayout = () => {
  // `Multi()` helper, aka wrapper tell us that we want to retrieve all providers saved to the `PAGES` token
  // `withMetadataKey` (temporary name of parameter) can resolve providers with returned Map, where key is metadata key, and value is resolved provider
  const pages = useComponent<{ [key: string]: React.JSX.... }>(PAGES, Multi({ withMetadataKey: 'path' }))

  return (
    <div>
      <Router>
        {Object.entries(pages).map(([path, Page])=> (
          <Route path={path} component={Page}>
        ))}
       </Router>
     <div>
  );
}

// extra component to use React Context to retrieve providers
const AppWrapper = () => {
  // as you can see, you can use also defined as provider component and use it in rendering process 
  const Layout = useComponent('Layout');

  return (
    <Layout />
  );
}

const App = () => {
    return (
      <Module module={...providers}>
         <AppWrapper />
      </Module>
    );
}

Of course, you have to remember that when you add a new component, you have to use it in another one to render it, or have such functionality in your application as routing and a defined token to pass components as providers (as standalone pages) :)

I want access to the "state" which I assume is the AsyncAPI file etc.

You can treat AsyncAPI doc also as normal provider and then reuse it in components (similar to React Context, but as provider) :)

// our providers
const providers = [
  {
    provide: AsyncAPIDocument,
    useValue: ... parsed AsyncAPI outside the providers array (use `useValue` to save constant value),
  },
];

const Component = () => {
  // inject value from provider
  const spec = useInject(AsyncAPIDocument);
  // ...logic
}

Or maybe do you wanna retrieve state from outside the AsyncAPI React component?

Also another note, you mentioned about getting access to internal state etc, did you consider or look at render callback patterns? I know they are abit "dated" but they do solve these kinda issues, when you want to expose state or the ability to do things within components themself.

Yeah, it's one of the solution, but the question is: how to use render callback pattern in non React app, like in Angular/Vue? We should support rendering in that frameworks with possibility to add/change components/providers. Additionally if you use this pattern then you have to return identical component each time - you want to change something in the state/read it then you still have to know what the component should return - if a user who only wants to know something about the state should know about it, I don't think so. And the last thing, how to wrap (decorate) the components with this pattern? It would be a little difficult for us to use this pattern on a larger scale.

Also, another side note, your provider pattern you mentioned above, I quite like that, a lot of libs/frameworks follow the standard provider pattern and pass things into it, like your one for example as the doc passed into it, I'm not sure that's a terrible solution πŸ€”, why don't you like the doc creation outside the provider?

Not that I don't like it, it is of course an option to pass an AsyncAPI document parsed outside the module/provider using useValue, but someone may need to transform the spec before rendering the component and then need other providers too, like:

const providers = [
  {
    provide: AsyncAPIDocument,
    useFactory: (transformers) => {
      // ...logic
    },
    inject: [ASYNCAPI_TRANSFORMERS] // you can inject other tokens in useFactory
  },
];

If you meant something else in this question, let me know :)

Do you have any other questions or do you see some inaccuracies?

boyney123 commented 2 years ago

Thanks @magicmatatjahu

Do you have any other questions or do you see some inaccuracies?

Yeah maybe it might be worth a video call on this, so you can walk through it. I think just because it's a new pattern I'm struggling to follow it a bit, but I like it from what I'm understanding from it.

What do you think? Maybe also we could record the session for others?

magicmatatjahu commented 2 years ago

@boyney123 Sure! As I wrote I have a working prototype, but I would like to implement some more mini things that I described here and then we can make a session :) I do not want to talk during the session about something that is still in the realm of idea and not reality πŸ˜…

char0n commented 2 years ago

Hello everybody,

I would like to join the discussion if I may, as SwaggerUI plugin system was mentioned couple of times here. It is worth mentioning from the start that SwaggerUI plugin system has been completely separated from SwaggerUI into reusable framework called – Swagger Adjust (https://github.com/char0n/swagger-adjust). Swagger Adjust is free of any OpenAPI specific code, all the pending bugs have been fixed, new features have been introduced and it has a new React API based on React hooks. I have written TLDR release article about it, which is available here: https://vladimirgorej.com/blog/how-swagger-adjust-can-help-you-build-extensible-react-redux-apps/

Some words to overall architecture – Swagger Adjust is based on ideas from Redux and functional programming. Core of the Swagger Adjust is called System and plugins works as enhancers to this System. Plugins compose in Swagger Adjust to create resulting plugin composition. Plugins are not aware of any other plugins. As with function composition, the order of provided plugins is important. Plugin state management is centered around Redux concepts.

Isn't this similar to the Plugins system in the Swagger UI? Yes, it's similar (I designed my proposal with the idea of Swagger plugins, but I was mainly looking at Angular implementation of DI), but Swagger UI's plugins have this problem that you cannot define when and for our cases it's very needed (see Contextual Injection).

Defining when is available on two levels:

  1. When the plugin is being composed, the plugin function receives System as the argument and can either return the plugin or void it

  2. Having the ability to wrap components, actions and selectors gives this conditional when mechanism

Additionally, plugins override components (not only them, but mainly) defined in previous plugins - it means that a component with the same name can override a previously used component - in my proposal, it will be possible to use a component from a given plugin.

Yes, this is true. Any plugin can override any symbol introduced to the System by any other plugin. I consider this more a feature than a drawback. It allows us to hook inside the system and override virtually anything exposed to the plugin system. I am wondering how this proposal would deal with the following situation:

One provider (A) injects another provider (B) into it. I want to define provider that redefines what B is. Is that achievable?

So, the modules themselves do not implement any contract, but you as a user can implement such a contract in your module and import it into our component and will be able to use it it in any place :) That's idea behind modules, and by this I dpn't want to use the Swagger Plugins approach, which has limitation in this case.

Can you maybe elaborate more on the limitation here? I am afraid I did not understand what the main point was here and what is the limiation of SwaggerUI plugin system in this context.

In summary – SwaggerUI plugin system does not have explicit mechanism to define explicit dependencies as this proposal have by using injects and doesn’t have explicit ability to define multiple values for the same symbol. It works more as an enhancer than a provider. The goal of this was to bring more clarity and motivation behind SwaggerUI plugin system and understand what others see as the β€œbad parts” about it and why.

magicmatatjahu commented 2 years ago

@char0n Hello! Thanks for that comment, It is always nice to read about the dilemmas of a solution in order to solve some problems or to see that a solution does not make sense/support a certain use case.

I started making research about "plugin" system April-May as I remember. I remember that I found info about Swagger Adjust in your blog post on Linkedin in which you presented Swagger Adjust and outlined the possibilities. For me, Swagger Adjust and Swagger Plugin system in general is a good solution if you look from React perspective. Here I would like to write that I have not dealt with the plugin solution since September, because I was absorbed in other things in AsyncAPI org, so I would have to refresh my memory.

However, I will respond to your comments as best I can. Please keep in mind that I didn't describe all the pros and cons of other solutions because I didn't want to write an elaborate article (but only proposal and start discussion), but only outline the main problems I found in other solutions, including Angular2+ DI. Please keep in mind that I may not know something about Swagger Adjust and its capabilities, please correct me if I talk lies.

Defining when is available on two levels:

  • When the plugin is being composed, the plugin function receives System as the argument and can either return the plugin or void it
  • Having the ability to wrap components, actions and selectors gives this conditional when mechanism

As I can see from the Swagger Adjust code there is a phase of "creating" plugins first and getting all the information from them including components, which components need to be extended (wrapComponents) and which need to be overwritten completely ("normal "components array), the same for actions/state in Redux. Later this data can be used in existing components etc.

If I correct understand that given "when" can override/change given data but globally, you cannot override (or better name will be decorate) given component in local place, for example only in one particular component, am I right? If not, please give me a example how to achieve that:

// providers
{
  provide: "Binding",
  useValue: MQTTBinding, // component which render the mqtt information
  when: (...params) => {...} // when binding is mqtt
},
{
  provide: "Binding",
  useValue: KafkaBinding, // component which render the kafka information
  when: (...params) => {...} // when binding is kafka
}

const BidningComponent = ({ bindingInfo, bidningName }) => {
  const Binding = useComponent('Binding', { bidningName }); // use `MQTTBinding` or `KafkaBinding` depending on bindingName

  return (
    <Binding {...bindingInfo} />
  );
}

As I see we have possibility to use system https://github.com/char0n/swagger-adjust/blob/main/src/system/index.js#L129 in the wrapper component but we don't have in the "injection" of given component the local context but global context represented by system. Also remember that in AsyncAPI we can have a different version for bindings, so render implementation for given binding should be based on AsyncAPI version spec, binding name and binding version. In proposed solution injection is more "atomic" so you can override given value in particular place and with given local context.

Also about: When the plugin is being composed, the plugin function receives System as the argument and can either return the plugin or void it. It's not included by this proposition, because I didn't want to overload proposal by ideas, but I want to make module/plugin registration lazy, "on premise", so in first stage DI system will load all metadata and then override everything what people want. Maybe I'm wrong, but as I understand from your mentioned sentence, "system" has only information from previous loaded plugin when register new one plugins, yes? If yes, then order of plugin in the registration process is important. In the projects like Studio we cannot "accept" such a logic, but it's a discussion for other topic.

Yes, this is true. Any plugin can override any symbol introduced to the System by any other plugin. I consider this more a feature than a drawback. It allows us to hook inside the system and override virtually anything exposed to the plugin system

In some situation it is not a good solution because you wanna have a several "definition" for given token and in some places retrieve all definitions, in some retrieve only one and the other filter by some context. In Java Spring you have such a Named (called as Qualifier) functionality when you wanna tell to DI system that you have several definition for given interface but you need particular definition. https://www.baeldung.com/spring-bean-names

I am wondering how this proposal would deal with the following situation: One provider (A) injects another provider (B) into it. I want to define provider that redefines what B is. Is that achievable?

Yep it is achievable in several ways. You should ask first if you wanna redefine it locally, globally or maybe in some "context" to share that provider to some other providers (not globally, not for single provider but for some "namespace", group of providers). I will only add example how to make it globally (I want to finish that comment πŸ˜… ). Also that system has features like "imports", "exports", so you have to explicit define that given provider is exposed to another modules where is imported or you wanna make it "private". Assume that provider A and B are components. A and B is exported from module. We wanna override (not wrap) provider B:

const moduleX = {
  providers: [
    {
      provide: 'A',
      useFactory: (b) => {...},
      inject: ['B']
    },
    {
      provide: 'B',
      ...
    }
  ],
  exports: [
    'A', 'B',
  ]
}

const mainModule = {
  imports: [
    {
      module: moduleX,
      providers: [
        {
           provide: 'B', // new definition for provider B
           ...
        }
      ]
    }
  ]
}

and then if we retrieve from DI system, from mainModule the A provider like:

const ComponentWhichUsesSystem = (props) => {
  const Acomponent = useComponent('A');
  ...
}

<Module module={mainModule}>
  <ComponentWhichUsesSystem />
</Module>

we will have injected new B into A. We can also wrap old B with new B by Decorate wrapper:

const mainModule = {
  imports: [
    {
      module: moduleX, // extend moduleX by additional providers. Angular has that same solution
      providers: [
        {
           provide: 'B', // new definition for provider B
           useWrapper: Decorate((Original) => {
             return function() {
               return <Original />
             }
           });
        }
      ]
    }
  ]
}

that second case maybe hard to understand but we can also introduce "wrapComponents" array for which we will perform that Decorate logic and that's same as wrapComponents in your Swagger Adjust but it's more atomic, and user have to explicit define that want to wrap, not override or add new definition.

Can you maybe elaborate more on the limitation here? I am afraid I did not understand what the main point was here and what is the limiation of SwaggerUI plugin system in this context.

I had to read the whole thread to understand what I meant then, hah πŸ˜… From what I understand we were talking about contracts that have plugins, in other words an interface that must be met. Swagger Adjust has an object shape that must be returned as a plugin { components, wrapComponents, fn.... } and this is the contract for the plugin. There is a contract for Module too, because it looks like this:

{
  imports: Array<Import>,
  providers: Array<Provider>,
  exports: Array<Import | Provider>,
}

This is also a contract, but you can also create sub-contracts like the example I gave David for ERROR_PLUGIN provider token. Is it possible to do like sub-system (sub-plugins with another contract) in Swagger Adjust?

In summary – SwaggerUI plugin system does not have explicit mechanism to define explicit dependencies as this proposal have by using injects and doesn’t have explicit ability to define multiple values for the same symbol. It works more as an enhancer than a provider. The goal of this was to bring more clarity and motivation behind SwaggerUI plugin system and understand what others see as the β€œbad parts” about it and why.

No problem :) Thanks very much for that comment and very helpful questions! They allowed me to determine if the current proposal makes sense for the long term and if it has any bottlenecks (and it has, like that overriding πŸ˜† , I have to rethink that part). As you wrote, we cannot define several values for a given "token", or make complex injections in Swagger Adjust. I think Swagger Adjust is very good project (but little known, which is very sad), but I lack those mentioned things.

And the most important thing which I did not write about in any other comment. As this is a DI system, it's not created only for React with Redux support (which is also a small disadvantage for me that Redux and not other stat system. Have you thought about possibility to change Redux to another state manager as an additional option?) but it's more treated as an API that can be integrated with React. What does that mean? That this system can also be used in NodeJS for e.g. CLI app, or as Express integration. For React we will integration and we can have also another integrations. Well, and last but not least possibility: you can create services like in OOP (DI like in Java Spring):

@Injectable() // indicates that given service can inject another providers/services
class SomeService {
  constructor(
    private specService: SpecService,
    private formatService: FormatService,
  ) {}

  ...
}

const ComponentWhichUsesSystem = (props) => {
  const someService = useInject(SomeService); // SomeService instance with injected SpecService and FormatService services
  ...
}

const providers = [
  SomeService,
  SpecService,
  FormatService,
];

<Module module={{
  providers
}}>
  <ComponentWhichUsesSystem />
</Module>

and that OOP part is very needed for our https://github.com/asyncapi/studio and also good support for TS is important to us, so it will be written in TS.

Hah, I wrote a lot of stuff. Thanks again for your comments and I look forward for feedback! I know that a lot of things are still unclear, but I don't have time to finish this proposal with working code that people can test.

char0n commented 2 years ago

Hi @magicmatatjahu,

Thanks for finding time to elaborate on this. This discussion is very valuable for me as it allows me to identify "bad parts" in SwaggerUI plugin system. I'll try to address all the individual points in hopefully understandable way.

If I correct understand that given "when" can override/change given data but globally, you cannot override (or better name will be decorate) given component in local place, for example only in one particular component, am I right? If not, please give me a example how to achieve that:

Giving your following example:

// providers
{
  provide: "Binding",
  useValue: MQTTBinding, // component which render the mqtt information
  when: (...params) => {...} // when binding is mqtt
},
{
  provide: "Binding",
  useValue: KafkaBinding, // component which render the kafka information
  when: (...params) => {...} // when binding is kafka
}

const BidningComponent = ({ bindingInfo, bidningName }) => {
  const Binding = useComponent('Binding', { bidningName }); // use `MQTTBinding` or `KafkaBinding` depending on bindingName

  return (
    <Binding {...bindingInfo} />
  );
}

I would incorporate it into the plugin system in following way, using React features itself to inject local context. People will have ability to override how specific bindings are rendered.

const MQTTBindingPlugin = () => ({
  components: {
    MqttBinding: () => 'mqtt binding',
  },
});

const KafkaBindingPlugin = () => ({
  components: {
    KafkaBinding: () => 'kafka binding',
  },
});

const BindingPlugin = () => {
  return {
    components: {
      Binding: ({ bindingName, bindingInfo }) => {
        const componentName = `${capitalize(bindingName)}Binding`;
        const BindingComponent = useSystemComponent(componentName);

        <BindingComponent {...bindingInfo} />;
      },
    },
  };
};

as I understand from your mentioned sentence, "system" has only information from previous loaded plugin when register new one plugins, yes? If yes, then order of plugin in the registration process is important.

Yes, the order of plugin registration is imporant. As the plugin system allows to override everything registered in it. Order defines the pipeline of plugins execution.

In some situation it is not a good solution because you wanna have a several "definition" for given token and in some places retrieve all definitions,

Yes this true, SwaggerUI plugin system doesn't allow registering the same symbol multiple times, but rather registring it again overrides the previous one.

Yep it is achievable in several ways. You should ask first if you wanna redefine it locally, globally or maybe in some "context" to share that provider to some other providers (not globally, not for single provider but for some "namespace", group of providers).

Looking at the code examples I take it's possible. THanks

This is also a contract, but you can also create sub-contracts like the example I gave David for ERROR_PLUGIN provider token. Is it possible to do like sub-system (sub-plugins with another contract) in Swagger Adjust?

Giving that my udnerstanding of this is correct, I don't think it's possible in Swagger Adjust. Every plugin is suppose to return the object in specified shape, which is the only contract that currently exists there.

As this is a DI system, it's not created only for React with Redux support (which is also a small disadvantage for me that Redux and not other stat system. Have you thought about possibility to change Redux to another state manager as an additional option?)

SwaggerUI Plugin System/SwaggerUI is build specifically for React usecase in mind and was never intended to be generic concept. Having said that, it gives us ability to use it's state management system via state plugins. But you can completly opt-out of using it and integrate your own state management system or any other existing one...If you need state manamagement system you can use the build-in one, or use your own. It's up to you.

but it's more treated as an API that can be integrated with React. What does that mean? That this system can also be used in NodeJS for e.g. CLI app, or as Express integration. For React we will integration and we can have also another integrations. Well, and last but not least possibility: you can create services like in OOP (DI like in Java Spring):

Yep, it's a generic concept, I got that from the initial description. SwaggerAdjust is specific for creating React apps.

Well, and last but not least possibility: you can create services like in OOP (DI like in Java Spring):

Using services should be possilbe as well using SwaggerAdjust, but injects would need to processed manually.

Given your following example:

@Injectable() // indicates that given service can inject another providers/services
class SomeService {
  constructor(
    private specService: SpecService,
    private formatService: FormatService,
  ) {}

  ...
}

const ComponentWhichUsesSystem = (props) => {
  const someService = useInject(SomeService); // SomeService instance with injected SpecService and FormatService services
  ...
}

const providers = [
  SomeService,
  SpecService,
  FormatService,
];

<Module module={{
  providers
}}>
  <ComponentWhichUsesSystem />
</Module>

I would achieve this in following way:

const ServiceAPlugin = () => ({
  rootInjects: {
    serviceA: {
      getData() {
        return 'A';
      },
    },
  },
});

const ServiceBPlugin = () => ({
  rootInjects: {
    serviceB: {
      getData() {
        return 'B';
      },
    },
  },
});

const ServiceCPlugin = ({ getSystem }) => {
  const { serviceA, serviceB } = getSystem();

  return {
    rootInjects: {
      serviceC: {
        getData() {
          return serviceA.getData() + serviceB.getData();
        },
      },
    },
  };
};

Of course it not a native concept, but can be acheived.

Thanks again for engagin in this conversation!

github-actions[bot] commented 2 years ago

This issue has been automatically marked as stale because it has not had recent activity :sleeping:

It will be closed in 120 days if no further activity occurs. To unstale this issue, add a comment with a detailed explanation.

There can be many reasons why some specific issue has no activity. The most probable cause is lack of time, not lack of interest. AsyncAPI Initiative is a Linux Foundation project not owned by a single for-profit company. It is a community-driven initiative ruled under open governance model.

Let us figure out together how to push this issue forward. Connect with us through one of many communication channels we established here.

Thank you for your patience :heart:

github-actions[bot] commented 1 year ago

This issue has been automatically marked as stale because it has not had recent activity :sleeping:

It will be closed in 120 days if no further activity occurs. To unstale this issue, add a comment with a detailed explanation.

There can be many reasons why some specific issue has no activity. The most probable cause is lack of time, not lack of interest. AsyncAPI Initiative is a Linux Foundation project not owned by a single for-profit company. It is a community-driven initiative ruled under open governance model.

Let us figure out together how to push this issue forward. Connect with us through one of many communication channels we established here.

Thank you for your patience :heart:

ThibaudAV commented 1 year ago

πŸ‘‹ I came across this Issues by chance and would like to know if it is still relevant ? whether it is still a proposal or whether work has started?

p.s : I find that when and the useFactory are duplicates. but πŸ€·β€β™‚οΈ The conditions of the when if there are any could be added in the useFactory and benefit from the injection too. There could even be abstract factories (by version or other)

magicmatatjahu commented 1 year ago

@ThibaudAV Hi!

I came across this Issues by chance and would like to know if it is still relevant ? whether it is still a proposal or whether work has started?

Still a proposal, it will most likely be easier to do, because the component does not need such complicated things.

I find that when and the useFactory are duplicates. but πŸ€·β€β™‚οΈ The conditions of the when if there are any could be added in the useFactory and benefit from the injection too. There could even be abstract factories (by version or other).

What you mean by this (as I understand from your comment), in some DI container implementation, is called controlled injection - e.g. based on some state of another provider you can create given provider on different way. Using when you have contextual injection so it means that based on some metadata you choose which implementation of provider you wanna inject (and create if provider isn't singleton). And yes, you're right that useFactory can have that "context" of when and reuse it, but it can be problematic for the front-end world. In front-end apps/components is very important to include only needed code, and using that when context is easier to handle that. Why? Check example:

{
  provide: 'some token',
  useFactory(context) {
    if (context.asyncapi === '3.0.0') {
      return <SomeComponentForV3 ...>
    }
    return <SomeComponentForV2 ...>
  }
}

as you can see you need to include implementation of components for v2 and v3 in single function. You don't have benefits of tree shaking. Even if you only need to render V3 or V2, you need all code. Using when you can write this:

{
  provide: 'some token',
  useValue: <SomeComponentForV3 ... />
  when(context) { return context.asyncapi === '3.0.0' }
}
{
  provide: 'some token',
  useValue: <SomeComponentForV2 ... />
  when(context) { return context.asyncapi === '2.0.0' }
}

and then these two components you can split to two arrays:

const v2Providers = [...]
const v3Providers = [...]

and in final bundle only include given array. I hope it's clear,

ThibaudAV commented 1 year ago

ok yes it is. thanks

And you're thinking of making this set of plugins with, I guess a core part, framework agnistic ? so that it can be fully integrated into the main frameworks? (as it is currently very react centred ) not sure if I am clear in my question 😁

magicmatatjahu commented 1 year ago

@ThibaudAV I was thinking to make it framework agnostic, as a separate DI package and then separate integrations for react, vue, even for some nodejs framework for backend application. I know that such a library as InversifyJS is still used by a lot of people but has been out of support for a few years now, so yeah, maybe my "idea" will attract some people then. But I still have to write this. If I will remember, I'll let you know when I publish such a package :) If, on the other hand, you mean whether these plugins will only be for the AsyncAPI component then the components will still be written in react, but "service" provider can be written in framework agnostic way. I hope that I answer to your question :)

github-actions[bot] commented 1 year ago

This issue has been automatically marked as stale because it has not had recent activity :sleeping:

It will be closed in 120 days if no further activity occurs. To unstale this issue, add a comment with a detailed explanation.

There can be many reasons why some specific issue has no activity. The most probable cause is lack of time, not lack of interest. AsyncAPI Initiative is a Linux Foundation project not owned by a single for-profit company. It is a community-driven initiative ruled under open governance model.

Let us figure out together how to push this issue forward. Connect with us through one of many communication channels we established here.

Thank you for your patience :heart: