Closed smoya closed 3 years ago
Hey! You've labeled this issue as a Scope. Remember you can use the following command to inform about its progress:
/progress <percentage> [message]
or
/progress <percentage>
A mutiline message.
It supports **Markdown**.
/progress 40 We're still figuring out how to implement this. We have an idea but it is not yet confirmed it will work.
/progress 50
A few notes:
* We got this figured out :tada:
* We're going to use [this library](#link-to-website) to avoid losing time implementing this algorithm.
* We decided to go for the quickest solution and will improve it if we got time at the end of the cycle.
:weight_lifting_woman: See the progress on the Shape Up Dashboard.
Well, this is quite a hard nut to crack...
Since this is the end of the workday I am just posting my initial thoughts on the task and potential solutions.
Different kind of signatures that we could implement, this should be followed throughout the API to ensure consistency:
i.e. doc.getMessageFor('mychannel', 'sub').payload()
This is still too close to the actual document -> channel -> operation -> message[0] -> payload. And what if we remove the message completely? Also, an operation can contain 0 to many messages, render this signature a bit weird.
i.e. doc.payload('mychannel', 'operation')
or doc.payload('mychannel', 'operation')
and doc.description('mychannel', 'operation')
.
We also know that the intent will most likely always be present.
Thoughts:
asyncapi.getChannelsByOperation('publish')
publish
. In some use-cases, it might be difficult to use since you have to make your own check whether you get 1 or more results. an operation can contain 0 to many messages
I was not aware of it. What is the use case for having no messages for an operation?
<intent>(... <nested object names>)
I'm not fully convinced of this approach. I know it was mentioned by @fmvilas in https://github.com/asyncapi/shape-up-process/issues/82, However, I think it will carry on with the same issues the current API has.
I suggest we define functions that map to intents but asking the minimum args as possible.
For example, for getting a message payload you just need to know the operationID
plus the "index" of the message (unfortunately we don't force to have a unique id per message that we could use instead). Also you could get all the messages for an operation.
asyncapi.message('operationID') // returns all messages for a given operation
asyncapi.message('operationID', 0) // returns the first message for a given operation
asyncapi.message('operationID', 0, 3) // returns the first and the third message for a given operation
Problem with variadic functions that we might encounter, especially in the implementation phase, what if a channel has the same name as an operation keyword such as publish. In some use-cases, it might be difficult to use since you have to make your own check whether you get 1 or more results.
Variadic functions (spread syntax in >=ES6) would not cause that issue if combined with rest parameters
In that case a function can look like function message(operation_id, ...indexes)
.
I was not aware of it. What is the use case for having no messages for an operation?
Because oneOf
is allowed to specify messages I assumed that it was 0 ... n
but playground gives an error π€
publish:
message:
oneOf: []
I'm not fully convinced of this approach. I know it was mentioned by @fmvilas in #82, However, I think it will carry on with the same issues the current API has. I suggest we define functions that map to intents but asking the minimum args as possible.
Hehe, I am not fully convinced about this approach either, I just wrote down my thoughts from yesterday, and I agree that we should use the minimum args as possible for all functions throughout the API. The only reason why I like the second approach more in general, if the intent is to get the payload
, is this:
If your intent is to get the payload of the message, getting it by using asyncapi.payload('operationID', ...)
is better then asyncapi.message('operationID', ...)
and then .payload()
since you cut out another step which might be removed (the message
object).
That being said, we do have intents which wants to get the message object itself (https://github.com/asyncapi/shape-up-process/issues/84#issuecomment-800935961) therefore we they must be able to go through that as well. It is just more prune to being obsolete. And this I think will probably be one the hardest tasks to figure out where that boundary is with intents π Since some of them are really, really close to the actual setup of the AsyncAPI spec.
Variadic functions (spread syntax in >=ES6) would not cause that issue if combined with rest parameters In that case a function can look like
function message(operation_id, ...indexes)
.
What I mean is that if we have a function which takes multiple arguments of same type it can be a problem distinguish them from one another. But maybe we wont have that problem, lets see π
From Slack thread, it seems there are some valid use cases for having zero messages.
That being said, we do have intents which wants to get the message object itself (#84 (comment)) therefore we they must be able to go through that as well. It is just more prune to being obsolete. And this I think will probably be one the hardest tasks to figure out where that boundary is with intents π Since some of them are really, really close to the actual setup of the AsyncAPI spec.
We do not aim for having the silver bullet API but rather something simpler and more resilient than what we have.
IMHO it is already a win moving from asyncapi.channel('mychannel').subscribe().message()
to asyncapi.message('operationID', ...)
or similar as you are not tied to the data structure but to the concepts declared in the spec.
I have a kind of different approach which is somewhat the same structure as we have now. I don't think we can make the API without agreeing on the underlying structure of the calls. For example asyncapi.message('operationID', ...)
is not resilient in it self, only If the structure behind the parser API does not change (that there will always be an operation id for example).
We need to make assumptions, assumptions that never change, if they do, a new major version of a parser is required. These assumptions are that regardless of how the specification declares the structure, the parser API will contain the same underlying structure.
I tried to keep the API structure as close to the current API (parser-js) as possible and are actually able to do so. I did this since the current structure makes sense from a tooling perspective and can be altered to support changes to the specification.
Descriptions for the API structure:
A
to point B
, example from document to messages you would have to go through asyncapi.channels(...).operations(...).messages()
(unless we have a shortcut from the document). ...
is about the properties such as description
etc. asyncapi
object merge info
object and nested objects into asyncapi (maybe alter naming of properties such as contact().name()
-> contactName()
). The same with license
properties.asyncapi
object merge externalDocs
into asyncapi
(maybe alter naming of properties)operation
object merge externalDocs
into operation
(maybe alter naming of properties)message
object merge externalDocs
into message (maybe alter naming of properties)hasX
functions to make it easier for tools. Related issue that should be fixed as well https://github.com/asyncapi/parser-js/issues/201Suggested API structure
asyncapi.servers()
, asyncapi.servers('<server name>)
server.variables()
, server.variables(<variable name>)
server.securities()
, server.securities(<security name>)
server.bindings()
, server.bindings('<binding protocol>)
asyncapi.tags()
, asyncapi.tags('<tag name>)
asyncapi.components()
, asyncapi.components('<component type>)
asyncapi.channels()
, asyncapi.channels(<channel name>)
, asyncapi.channels(<channel name>, ...)
channel.bindings()
, channel.bindings('<binding protocol>)
channel.parameters()
, channel.parameters('<parameter name>)
channel.operations()
, channel.operations(<operation type>)
operation.tags()
, operation.tags('<tag name>)
operation.messages()
, operation.messages(<message index>)
message.header()
?message.payload()
message.tags()
, message.tags('<tag name>)
message.bindings()
, message.bindings('<binding protocol>)
message.examples()
?I have not included all intent functions in this suggestion, many can be thought of such as asyncapi.messages(<channel name>, <operation type>)
and asyncapi.messages(<channel name>, <operation type>, <message index>)
. But we have to start somewhere.
I am not 100% sure all changes to the underlying specification will be possible to "safe guard" here. But that is yet to be determined.
But Jonas, that is the same structure as we have now, what changed? Yes it is (somewhat) the same structure that we have now, the only difference is that you have to change the way you think about it. The API structure is now completely detached from the underlying specification structure, and remains the same regardless of changes to the specification.
Any thoughts or doubts about this underlying structure? Does it make sense? :thinking:
Just to mention, there is a request to add messageId to spec. https://github.com/asyncapi/spec/issues/458
And then asyncapi.message('messageId')
would be possible.
Thanks, @jonaslagoni for your great work on that suggestion. You made a really great point:
We need to make assumptions, assumptions that never change
If we stick with that as our main goal, we could reach an API that will be (quite) resilient to breaking changes. You mentioned going with an approach similar to what we have today but with some light changes. That made me think a bit on it more deeply, and discovered this won't represent the real user intentions and will keep breaking easily as well.
I'm gonna try to throw some ideas about what I think the approach can be (based partially on what Fran mentioned).
We have to first find who are the potential users of the parser. Some examples:
As you can see, those intents could be transformed into actual API methods and they will be valid (almost) forever. For example:
From the application point of view:
asyncapi.messagesTheApplicationPublishes()
From the client point of view:
asyncapi.messagesAClientCanSubscribe()
The interface is gonna stay unbreakable between versions, unless a really big refactor happens on the spec, such as a completely redefinition or renaming. The implementation, however, could change. But that's not an issue. A new version of the parser will be released adding the needed changes inside the functions. Users will just need to update to the new version without changing their code.
I can't ellaborate a more detailed suggestion right now as I'm reaching my EOD here. Will keep iterating next Monday.
/progress 10 We have been looking for different API approaches and added a couple of suggestions. We are looking for setting the API foundation based on intents.
Thanks for the feedback @smoya!
That made me think a bit on it more deeply, and discovered this won't represent the real user intentions and will keep breaking easily as well.
How come π€? For the intents asyncapi.messagesTheApplicationPublishes
and asyncapi.messagesAClientCanSubscribe
it works perfectly, since it does not need information about the underlying structure. But now you have received an array of message
objects. Now what, if we have not declared the structure of that object what would it return? Is that depending on the spec version that is passed along? π€
This is what I mean by the underlying structure of the API, we still need structure behind the scenes regardless of what intents we have. Therefore I suggest that we decouple the parser API from the spec in a way that makes sense and is resilient to changes (i.e. we iteratively use your suggestive changes to the spec as validating it wont break the API, if it does we then have to revise it and see if we can make it more resilient).
But now you have received an array of message objects. Now what, if we have not declared the structure of that object what would it return? Is that depending on the spec version that is passed along?
Yes, we would need to keep the getters for each field. For example, message.payload()
. However, in case of child objects, perhaps we would need to think on intents again. It may happen that it does not fit as intent either and becomes another getter. That's ok.
A note on those getters: Those are needed due to the nature of the language we are using as first implementation (JS) but this could be gone in another implementation such as the Go parser one. However, intents will always be there no matters what language we use. That's the key point of all of this.
Does it make sense? WDYT?
Yes, we would need to keep the getters for each field. For example,
message.payload()
. However, in the case of child objects, perhaps we would need to think on intents again. It may happen that it does not fit as intent either and becomes another getter. That's ok.
I think we are beating around the bush and talking about the same thing π
When we talk implementation it means that we need a file for message
which contains all the message properties we have (accessed through getters). We need an operator
class that contains all properties for operators.
The link from the root object asyncapi
to message
is our intent right? I.e. how we go from point A to point B.
Also However, in case of child objects, perhaps we would need to think on intents again
this infers that we know which objects are a child of message
. i.e. we imply an underlying structure. One of those structures is what I proposed, i.e. we acknowledge that channels
are under asyncapi
, operations
are under channels
and messages
are under operations
.
This still means that we can have intents such as asyncapi.messagesTheApplicationPublishes
or asyncapi.operationsTheApplicationPublishes
or whatever we figure out we need π
Does this make sense or? π€
this thread is getting long π and I have to be honest I didn't read all. My question to proposal based on example:
why do I have to do message.bindings('kafka')
(before that, I have to access channels and operations) and can't have getBindings('server', 'kafka')
or getServerBindings('kafka')
also small comment to proposal that it would be useful to see in what arguments of the function are optional
@derberg yea, sorry π
why do I have to do message.bindings('kafka') (before that, I have to access channels and operations) and can't have getBindings('server', 'kafka') or getServerBindings('kafka')
You won't "have to", what I proposed is the basic structure that needs to be in place for you to iterate messages, servers, channels, etc. And then what getter methods you will have access to from those objects.
Intents are as I said a pathway to how you come from A to B. In order for getServerBinding('<specific server>', 'kafka')
to make sense, you imply that the server object contains a binding property. This also means that it should be possible to access those bindings when you are iterating servers.
We can after the basics are down add better intent functions such as those you describe there to better access the underlying properties.
you imply that the server object contains a binding property
That's not true. You are not implying the structure behind but just the intention. This is a clear example of thinking out of the box. We need to focus on the real user intention which is: "I need that Kafka binding". The user should not case about the data structure behind, it does not matter if the binding lives under server or whatever.
why do I have to do
message.bindings('kafka')
(before that, I have to access channels and operations) and can't havegetBindings('server', 'kafka')
orgetServerBindings('kafka')
You are basically declaring your intent in there. This will totally fit within the suggestion I made. π
I'm gonna try to come up with a set of clear examples on the approach I suggested.
you imply that the server object contains a binding property
I'm not implying anything here π I just want to know what are bindings for my server. I simplified it as of course I can have more than 1 server so I should be able to specify the server name, etc. It would not change my point though
API just implies that I have access to 3 different bindings getBindingsServer
, getBindingsMessage
, getBindingsOperation
, but if those operations are under bindings or bindings are under operations π€·πΌ I don't know and I don't have to know. My IDE should be the source of truth, if I have getBindings
, API docs jus tell me what options I have. I should even have hasBindings('kafka')
so I can easily check if document has 'kafka' bindings that I do not support and can easily prompt user, like with hasCircular
.
This is my understanding of intend. Like here https://github.com/asyncapi/nodejs-ws-template/blob/master/template/src/api/services/%24%24channel%24%24.js#L31, I just want the payload type, getPayloadType()
, I don't care where it is in the document, but I know what message is it related too
@magicmatatjahu @jonaslagoni and @smoya (me) had an open meeting where we discussed the following topics:
GroupID
field from Kafka operation binding. Let's imagine the spec changes and the binding ends moving that field to another place like channels.asyncapi.bindings('kafka', 'operations', 'opID1').groupID()
=> asyncapi.bindings('kafka', 'channels', 'channelName1').groupID()
channel.operation.[0]
-> operations[0]
The recordings of the meetings can be downloaded here:
Next steps on this would be:
Servers
and start creating some code mockups. representing all the intents and the rest of needed methods./progress 25
We have been sharing different approaches and it seems we have one that can be used as base:
However, several good points were made on https://github.com/asyncapi/shape-up-process/issues/90#issuecomment-804845905 and we are waiting for another pass by @jonaslagoni based on the intents we recap in https://github.com/asyncapi/shape-up-process/issues/84. This will be useful for validating the suggestion.
I have gathered all the intents from https://github.com/asyncapi/shape-up-process/issues/84 and tried to structure different intents we can start out with. These are just a starting point and are no way near final. So look at the example with the thought about whether this can be adapted as our general approach to any intents.
All intents will have a has<intent>
function attached to them which returns a boolean.
All intents are described using the following format:
<definition>.<has/get><intent>(<potential arguments>) : <return type> (based on which use-case)
definition
of the AsyncAPI documentdefinition
which we agree will always be there (if not, it will force a major version of the parser). This definition will contain a set(subset) of intents.definition
which we agree will always be there (if not, it will force a major version of the parser). This definition will contain a set(subset) of intents.definition
which we agree will always be there (if not, it will force a major version of the parser). This definition will contain a set(subset) of intents.definition
which we agree will always be there (if not, it will force a major version of the parser). This definition will contain a set(subset) of intents.The following describes the different definitions and their corresponding intents.
These are the intents defined in the issue but deemed not relevant to the parser.
getMessagePayload
AsyncAPIDocument.hasContentType('<content type>')
: boolean (based on comment(s): 799481319)AsyncAPIDocument.getTitle()
: string (based on comment(s): 799481319, 799598596, 800282407, 800963792)AsyncAPIDocument.getDescription()
: string (based on comment(s): 799481319, 799598596, 800282407)AsyncAPIDocument.getVersion()
: string (based on comment(s): 799481319, 799598596, 800282407, 800963792)AsyncAPIDocument.getMessagePayload('<channel name>', '<operation type>')
: Schema[]AsyncAPIDocument.getMessagePayload('<channel name>', '<operation type>', <message index>)
: SchemaAsyncAPIDocument.getMessagePayload('<channel name>')
: Schema[]AsyncAPIDocument.getChannels()
: Channel[] (outcome of comment(s): 800282407, 800935961)AsyncAPIDocument.getServerProtocol('<server name>')
: string (outcome of comment(s): 800282407)AsyncAPIDocument.hasChannelWithOperation('<channel name>', '<operation type>')
: boolean (outcome of comment(s): 799481319)AsyncAPIDocument.getChannelNames()
: string[] (based on comment(s): 800282407)AsyncAPIDocument.getBinding('<binding protocol>', '<binding property>')
: any (outcome of comment(s): 799481319, 799598596)AsyncAPIDocument.getBindingForOperation('<operation id>', '<binding protocol>', '<binding property>')
: any (outcome of comment(s): 799481319, 799598596)AsyncAPIDocument.getBindingForOperation('<operation type>', '<binding protocol>', '<binding property>')
: any (outcome of comment(s): 799481319, 799598596)AsyncAPIDocument.getBindingForChannel('<channel name>', '<binding protocol>', '<binding property>')
: any (outcome of comment(s): 799481319, 799598596)AsyncAPIDocument.getChannelDescription('<channel name>')
: string (based on comment(s): 799481319, 800282407)AsyncAPIDocument.getSummaryForOperation('<operation id>')
: string (based on comment(s): 800282407)AsyncAPIDocument.getMessages()
: Message[] (outcome of comment(s): 800935961)AsyncAPIDocument.getSecuritySchemaComponents()
: SecuritySchema[] (outcome of comment(s): 800935961)AsyncAPIDocument.getSchemaComponents()
: Schema[] (outcome of comment(s): 800935961)AsyncAPIDocument.getSchemas()
: Schema[] (outcome of comment(s): 800935961)AsyncAPIDocument.getParameters()
: Schema[] (outcome of comment(s): 800935961)AsyncAPIDocument.getServer('<server name>')
: Server (outcome of comment(s): 800935961)AsyncAPIDocument.getJSON()
: Server (outcome of comment(s): 800935961, 800963792)AsyncAPIDocument.getExtension('<extension property>')
: any (outcome of comment(s): 799598596)AsyncAPIDocument.getExtensionForChannel('<channel name>', '<extension property>')
: any (outcome of comment(s): 799598596)AsyncAPIDocument.getExtensionForOperation('<operation id>', '<extension property>')
: any (outcome of comment(s): 799598596)AsyncAPIDocument.getExtensionForOperation('<operation type>', '<extension property>')
: any[] (outcome of comment(s): 799598596)Channel.hasOperation('<operation type>')
: boolean (based on comment(s): 799481319, 800282407)Channel.getMessageHeaders('<operation type>')
: Schema (based on comment(s): 800282407)Operation.getBinding('<binding protocol>', '<binding property>')
: any (outcome of comment(s): 799481319, 799598596)Operation.getOperationId()
: string (based on comment(s): 800282407)Operation.getSummary()
: string (based on comment(s): 800282407)Operation.getExtension('<extension property>')
: any (outcome of comment(s): 799598596)Channel.getBinding('<binding protocol>', '<binding property>')
: any (outcome of comment(s): 799481319, 799598596)Channel.getDescription()
: string (based on comment(s): 799481319, 800282407)Channel.getExtension('<extension property>')
: any (outcome of comment(s): 799598596)Channel.getExtensionForOperation('<extension property>')
: any (outcome of comment(s): 799598596)I still need some time to look at it closely but for no I found a duplication here:
AsyncAPIDocument.getBindingForOperation('<operation id>', '<binding protocol>', '<binding property>') : any (outcome of comment(s): 799481319, 799598596)
AsyncAPIDocument.getBindingForOperation('<operation type>', '<binding protocol>', '<binding property>') : any (outcome of comment(s): 799481319, 799598596)
and because of it, you are missing getBindingForMessage
@derberg it is not a duplicate, one uses operation id
and the other uses operation type
π I am not sure how use full both are though.
AsyncAPIDocument.getMessagePayload('<channel name>')
This one is not going to age well. If we end up adding support for Message Id (and most probably we will) we'll also want to have a function like AsyncAPIDocument.getMessagePayload('<message id>')
. In both cases, this is a string, making it impossible to distinguish the intent. I'd be careful with these variadic functions.
AsyncAPIDocument.hasContentType('<content type>')
What about getContentType()
? I think it's missing.
AsyncAPIDocument.getChannelNames()
What about getChannel(<channel name>)
? I think it's missing.
AsyncAPIDocument.hasChannelWithOperation('<channel name>', '<operation type>')
Watch out! If we change the meaning of "publish" and "subscribe", we're screwed here. Try to capture a bit more the intent. For instance, something like:
AsyncAPIDocument.isSubscribingToChannel('<channel name>')
AsyncAPIDocument.isPublishingToChannel('<channel name>')
AsyncAPIDocument.clientCanPublishToChannel('<channel name>')
AsyncAPIDocument.clientCanSubscribeToChannel('<channel name>')
These four methods will:
a. Express the intent of knowing if my application is subscribing to a channel. Our implementation will look for publish
or subscribe
depending on its meaning.
b. Express the intent of knowing if I βas a clientβ can publish or subscribe to a channel.
AsyncAPIDocument.getBindingForOperation('<operation type>', '<binding protocol>', '<binding property>')
Same as above. This signature will break if the meaning of publish/subscribe changes. I'm assuming <operation type>
is either "publish" or "subscribe".
AsyncAPIDocument.getExtensionForOperation('<operation type>', '<extension property>')
Same as above.
Channel.hasOperation('<operation type>')
Don't assume channels have operations. It's a mistake we did in the past. This issue will most likely decouple channels and operations.
Channel.getMessageHeaders('<operation type>')
This method most likely belongs to Operation instead. Conceptually (forget about AsyncAPI here), a channel does not have any operation associated. Instead, an operation has a channel associated to it. In general, I think you also should avoid explicit mentions to <operation type>
, for the semantic reasons describe above.
AsyncAPIDocument.getSecuritySchemaComponents()
Typo. It's SecurityScheme.
Channel.getExtensionForOperation('<extension property>')
I think this one is missing the operation id.
Couldn't read all of the above. But I'm thinking there should also be something:
Message.getMessageId()
and also AsyncAPIDocument.getOperations()
All of them will be like,
AsyncAPIDocument.getChannels()
AsyncAPIDocument.getOperations()
AsyncAPIDocument.getMessages()
AsyncAPIDocument.getSchemas()
Just FYI I am taking all of your thoughts into consideration and gonna write another suggestion based on those instead of addressing them one by one.
@derberg it is not a duplicate, one uses
operation id
and the other usesoperation type
π I am not sure how use full both are though.
How are you gonna differentiate the argument from one function signature to the other if they have the same type?
How are you gonna differentiate the argument from one function signature to the other if they have the same type?
You can do a couple of things behind the scene to check whether it is an operation type/operation id, but based on @fmvilas reply it seems like we need to remove operation type
completely and instead place it in the intent... Gonna fiddle around with some ideas and post them when they are ready. @smoya feel free to also post your comments on the suggested API as well π
I don't see now the benefit of moving the property name you wanna get to the arguments.
If we can make the assumption that the returned Binding Object is gonna be always a map, then we could access the fields directly through the map.
For example: AsyncAPIDocument.getBinding('<binding protocol>').binding_property()
. Which could also translate to doc.binding('<binding protocol>')["binding_property"]
in Golang as an illustrating example.
On one side, I would like to suggest we remove the get
prefix for all the methods. Not sure how idiomatic it is in terms of Javascript but it is common in other modern languages. It is implied the operation you do when calling a function of this library, a parser, that you will mostly get information and never edit it (set
operations?).
I also wanna make a formal suggestion based on some of the methods I added in https://gist.github.com/smoya/1d81a9556b8756c883c44ce873b645f1 :
AsyncAPIDocument.channelsApplicationPublishesTo() : Channel[]
AsyncAPIDocument.channelsApplicationSubscribeTo() : Channel[]
AsyncAPIDocument.channelsClientCanPublishTo() : Channel[]
AsyncAPIDocument.channelsClientCanSubscribeTo() : Channel[]
AsyncAPIDocument.messagesApplicationPublishesTo() : Message[]
AsyncAPIDocument.messagesApplicationSubscribeTo() : Message[]
AsyncAPIDocument.messagesClientCanPublishTo() : Message[]
AsyncAPIDocument.messagesClientCanSubscribeTo() : Message[]
So we can break the confusion barrier of https://github.com/asyncapi/spec/issues/520 and reduce the magnitude of the Operation object, becoming less important for the user.
Considering those intents could also apply in the context of each collection (e.g. Channels array), I'm thinking if it would make more sense, from a pure technical point of view, to avoid using arrays and instead use iterable objects, so each array would become an object of a class with itents as methods. For example, using JS:
class Channels {
constructor(channels) {
this.channelsArray = channels;
}
[Symbol.iterator]() { return this.channelsArray.values() } // Not sure if this is exactly how you do an iterable
channelsApplicationPublishesTo() {
}
// ...
}
I'm missing functions that would be used in templates when rendering multiple files (with file templates for example). Basically what would, for example, $$channel$$.js
get in context? or would context
become obsolete?
Operation.getSummary() and Operation.getOperationId()
why not just AsyncAPIDocument.getOperationSummary()
and AsyncAPIDocument.getOperationId()
?
why not just AsyncAPIDocument.getOperationSummary() and AsyncAPIDocument.getOperationId()
Would you do this for all the fields within the objects?
I would say yes, just looking at the current structure of the operation, we anyway would like to have dedicated functions for getting a binding basing on operationId or getting a message the same way, so why not having for other fields
current template code (warning: some Nunjucks stuff ahead):
{% for channelName, channel in asyncapi.channels() -%}
{%- if channel.hasSubscribe() %}
{%- if channel.subscribe().summary() %}
some logic
{%- endif %}
{%- endif %}
{%- endfor %}
what about:
{% for operationId in asyncapi.getSubscribeOperationsIds() %}
{% if asyncapi.hasOperationSummary(operationId) %}
{{ asyncapi.getOperationSummary(operationId) }}
{% endif %}
{% endfor %}
getSubscribeOperationsIds()
what? ya crazy? π
in templates, we iterate a lot over arrays, just to pick up something by an id, then why not having helpers that just return only Ids collections? JS for a reason exposes API like Object.keys(object1)
warning: I was inventing here while writing the comment, so there might be wrong assumptions, but the code of the current template is a real one and you can see that I need to go through a lot to just grab a summary π€·πΌ
getSubscribeOperationsIds() what? ya crazy? π
In the case of Operations, I think getting operations from the root level (by root level I mean the methods on the AsyncAPIDocument
) can work.
However, my concern and the reason I would say we should avoid doing that for absolutely all fields/properties is the (big) amount of methods we will have at the root level.
In the end, if you expose operation Ids, why wouldn't you also expose any other field around our models, like for example, βthe message title?.
I was thinking about segregating intents and methods across models rather than add all of them at the same root level.
By model, I mean models that represent our domain, such as channels, messages, etc., and not the current spec data hierarchy.
Meaning Messages will be queried from the root level (e.g. AsyncAPIDocument.messagesApplicationSubscribeTo()
), but not their properties. Instead, their properties will be available as either intents or just getters. E.g. message.title()
or messages.titles()
.
I think @jonaslagoni will add more light on this soon as we have been trying to elaborate an assumption based on that, just to make things simpler, especially for newcomers.
As a side comment, I think it will be easier to move this conversation to a PR. Easier to leave comments on lines and have discussions there. Plus, once you make changes, we all will see what has changed.
@smoya and I have added a PR to the parser with an updated suggestion for the API - https://github.com/asyncapi/parser-js/pull/263
We are keeping it in the PR as you suggested @fmvilas to enable better feedback.
/progress 40 We have created a PR that contains a draft of the Parser API. However, the suggested API is not as resilient to breaking changes as we desired. @jonaslagoni applied the emulated breaking changes from shape-up-process/issues/93 and figured out some issues we may face. See the test results here.
/progress 60 we have defined the mock intent API and started integrating it with tools to validate the API holds up.
/progress 70 New API validated against markdown-template. Diff of changes to adapt it to the new Parser API can be found here
Also new methods has been suggested as addition to the API definition that were required by the markdown-template: https://github.com/jonaslagoni/parser-js/pull/1
Those are gonna be the next steps:
master
branch.I have been investigating a solution for defining, with a kinda metalanguage, our API definition so it can be given to developers of new Parser implementations (Go, Rust, whatever). I could not find any silver bullet solution besides using UML diagrams (we can consider).
An incomplete example of using PlantUML dsl for generating a UML for our API can be found here
As alternative, We could keep a document similar to https://github.com/asyncapi/parser-js/pull/263/files#diff-b57590968a12cee85a37c1b91d8cc7092cd8b68e50b242c39a178121db82a797 as Development guidelines for new Parser implementations, and instead, use the generated TS types for specific JS users.
Any ideas are welcome anyway.
/progress 75 We have been added new intents to the API documentation so we cover all the use cases we consider validated.
As far as I see, next steps are:
/progress 90 We updated the documentation of the new Intent-driven API and added all missing intents.
/progress 95 We have created a new repository that will contain the new Parser(s) API. There is a PR on it with the new intent-driven API specification and some design documents: https://github.com/asyncapi/parser-api/pull/1
/progress 100 Parser API v1.0.0-alpha got documented: https://github.com/asyncapi/parser-api π π π π
We want to start defining the new Intent-Driven API for the parser(s). Based on the current API Intents, define an API style having in mind the following (at least) requirements:
1. Intent-driven
Focus always on the final user intention rather than on a particular need on the code. Examples:
publish
). This could look likeasyncapi.getChannelsByOperation('PUBLISH')
asyncapi.getMessageFor('mychannel', 'subscribe').payload()
2 Versioned
Introducing API versioning lets users stick to a version and ensuring their code will keep working.
3. Resilient to breaking changes
API breaking changes are gonna happen. However, we want to let users embrace those smoothly. API methods should be written using signatures that can ensure compatibility within 2 versions or more.
Example:
Accessing the payload of a message in a
subscribe
channel looks like the following today:Let's imagine we want to rename the operations
publish/subscribe
topub/sub
.As this is tied to the nature of the data structure behind it. It is completely coupled to the structure of the AsyncAPI document. Any change on such structure (changing deep, order...) will introduce a breaking change on the API.
In case we want to support the old method for a while (as deprecated), will mean we would maintain two methods on our API:
subscribe()
(deprecated)sub()
Resilient version
Will turn into
This introduces two pros:
asyncapi.getMessageFor('mychannel', 'subscribe').payload()
could still work for a couple of versions.4. User-friendly means simple
Examples:
As a variadic function, this one would support:
asyncapi.servers()
asyncapi.servers(server1name)
asyncapi.servers(server1name, server2name)
[More examples to come]