-
-
Notifications
You must be signed in to change notification settings - Fork 126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal to define Modules aka Plugins for UI component and Studio (Editor) #433
Comments
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: 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 I'm thinking the 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 @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 😃 |
@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 had this doubts too, whether to share or not, but I remember one particular comment in one issue (#394 (comment)) 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 :) 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
Yes and no 😄 On one hand we will provide this 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 :)
You can still use the 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):
And the 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. |
@magicmatatjahu awesome, thank you so much for your comment as well 😃. I understand the module contract's approach as well 👍. Basically, we are giving users flexibility and a choice, not enforcing a contract on them 👍.
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? |
@BOLT04 No problem and thanks!
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. |
That sounds like a solid suggestion 👍
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. |
Sure, the complexity of the modules logic itself won't be easy, but the usage won't be that difficult to understand - I hope 😅 |
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 Also, another side note, your |
@boyney123 Hi! Thanks for comment!
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:
// 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) :)
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?
Yeah, it's one of the solution, but the question is: how to use
Not that I don't like it, it is of course an option to pass an AsyncAPI document parsed outside the module/provider using 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? |
Thanks @magicmatatjahu
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? |
@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 😅 |
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.
Defining
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?
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 |
@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.
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:
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
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 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 const ComponentWhichUsesSystem = (props) => {
const Acomponent = useComponent('A');
...
}
<Module module={mainModule}>
<ComponentWhichUsesSystem />
</Module> we will have injected new 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
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 {
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
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. |
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.
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 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} />;
},
},
};
};
Yes, the order of plugin registration is imporant. As the plugin system allows to override everything
Yes this true, SwaggerUI plugin system doesn't allow registering the same symbol multiple times,
Looking at the code examples I take it's possible. THanks
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.
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
Yep, it's a generic concept, I got that from the initial description. SwaggerAdjust is specific for creating React apps.
Using services should be possilbe as well using SwaggerAdjust, but injects would need to processed 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! |
This issue has been automatically marked as stale because it has not had recent activity 😴 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 ❤️ |
This issue has been automatically marked as stale because it has not had recent activity 😴 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 ❤️ |
👋 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 : |
@ThibaudAV Hi!
Still a proposal, it will most likely be easier to do, because the component does not need such complicated things.
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 {
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, |
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 ) |
@ThibaudAV I was thinking to make it framework agnostic, as a separate DI package and then separate integrations for |
This issue has been automatically marked as stale because it has not had recent activity 😴 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 ❤️ |
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 aconfig
prop to the component: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 (#305) like:
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 😄
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 likeand 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:
where:
{TOKEN}
token is an integral representation of the factory - it can be treated as a factory ID that can be used in the code to retrieve the value from the factory assigned to the token. It can be reference to the class definition, string or JS symbol. It is also possible to use token called InjectionToken, which has the advantage that with TS we can tell what value the factory should return - better typing in the code.{KIND_OF_FACTORY}
can beuseFactory
,useValue
,useClas
,useExisting
- each has a slightly different character but most are sugar syntax onuseFactory
:useFactory
- function that returns some value to the system by{FACTORY}
useValue
- sugar syntax for useFactory that return const valueuseFactory: () => some_value
useClass
- factory that creates instance of a class - it can be service which has some helpers needed in component/moduleuseExisting
- it's exactly alias for another provider, so if you want to use theTOKEN_1
but you make alias to it byTOKEN_2
you exactly get the value from providerTOKEN_1
{WHEN}
is a function that gets the metadata where something should be used and determines whether to return the value or skip it.Worth to mention is that
useFactory
anduseClass
can use another values from system (before creation) byinject
field, when you define tokens from system: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:
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:for version - when we have breaking changes in some parts that cannot be easily handled by a lot of
if
s:for bindings - depending on the binding type and where it is defined (serverl/message etc...), we can use a completely different component to render the content:
Of course
when
will work also for normal values and factories (which are not components):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:
...and then
ComponentWhichUsesSystem
component can use the values fromModule
byuseInject
anduseComponent
.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:
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
andexports
. 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.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:
Wrappers - as I mentioned in the introduction, the problem is the inability to override/wrap a component. By wrapper you can wrap some original provider/component, decorate or intercept it and return, like:
Multi injections - inject all possible factories for a given token. This is usually needed when you want to render a list of some components. It will be useful in our Studio where we will be adding integrations to Optimizer, Cupid etc. - then the appropriate icon will appear in the sidebar.
Scopes - by default each provider is singleton, but if someone wants, can create on every injection new instance/value.
FAQ
Couldn't React context have been used instead of DI/System?
Could be, but it has some limitation and due to this it's not a good idea. Of course, internally React treats context as simple DI system, but one issue is problematic.
The value for the React context is static, it must be initialized when initializing the context itself. You cannot reuse value from given context in the another value from this same context - only children components can. Additionally, you can't use a function to initialize a value because, as I wrote, everything is static. So instead of this:
you must parse document outside the context and save it to it:
Additionally, values from context are shared downwards for children, and cannot be used upwards - we want to share state.
In fact, the System/DI is a wrapped context that removes these limits and adds many other possibilities, which is described in proposal.
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). 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.Is it fast?
Not as much as regular React context, but it's not that slow again. If the value for a given token is created for the first time, then it is cached and reused. If it is a singleton, if not creates value from scratch, see Other possibilities
Will it be possible to use it in the Studio?
In the Studio it will show its capabilities to the limit - I hope 😄
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 😄
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.
The text was updated successfully, but these errors were encountered: