Thank you for watching the presentation about API-First approach k6 extension development. Let's see what this presentation will be about.
The first part is a use case, from which it becomes clear what the API-First approach is in terms of k6 extension development. The second part is a brief technological overview of what's under the hood. How code generation works, what were the design considerations. The last part is a live demo, using code fragments from the first part.
This may be subjective, but let's see what I want as a k6 extension developer and what I don't: I would like to define the extension API in an IDL. Since the de facto IDL in the case of JavaScript is the TypeScript declaration file, I would like to describe the API using it. I would like go interfaces and methods to be automatically generated from the API description, and I would only have to implement them. I would like the API definition to be the primary source of documentation as well. --- I don't want to write boiler plate code. Nobody likes to write boiler plate code. I don't want to understand how the JavaScript engine and the go runtime fit together, I don't want to understand the bindings. As a go developer, I don't want to use Node.js based tools. Not because I don't like it, but because it would significantly increase the complexity of a go project.
As an example, let's look at a simple, completely useless extension. There is also a more usable extension in the examples directory, such as a simplified version of the faker extension. The example should fit on one slide, so I chose this example. What we see is a TypeScript declaration file. It defines a class called "Guide", with two properties, one method, and one constructor. The type of the "question" property is string, writable and readable. The type of the "answer" property is "int" and is read-only. Since there is no int type in TypeScript (and JavaScript), all numbers are of type "number". The "number" type maps to the "float64" go type by default. However, I want this property to be of type "int" in go, so I created a type alias from "int" to "number". As a result, the type "int" will appear in the generated go interfaces. In addition to the Guide class, the API definition also defines a Guide instance as a default export.
Instead of writing boring boiler plate code, it is a good practice to use a code generator. The following command generates the go interfaces to be implemented based on the API definition and optionally an empty skeleton implementation. The skeleton file contains a special "skeleton" go build tag, so it is not involved in the go compilation. The real implementation is placed in another file. In this way, code generation can be run at any time after the API changes. The skeleton file can be used to help implement API changes.
The go interfaces generated by the code generator can be matched almost one-to-one with those included in the API definition. In addition to the defined interfaces, an interface called "Module" is also created, which represents the extension as a JavaScript module. It contains the module-level variables, functions, the default export and the factory methods belonging to the constructors of each class. The binding is included in the generated code, so the developer just needs to focus on the implementation.
Huh, that seems like a lot to implement a simple extension. Note, however, that this is the complete implementation code, with extra features such as readonly property and type mapping. On the other hand, the goal is not to minimize the implementation code, but to simplify the development of the extension. This implementation is made from the generated skeleton with minimal modifications. The code is about 90% identical to the generated skeleton. Sorry if it's hard to read, this example can also be found in the examples directory of the GitHub repository. I put it on a slide so that we have an impression of what needs to be implemented. On the other hand, I will use this as a cheat sheet in the live demo.
Here is a simple integration test that demonstrates how to use the example extension in k6. The test uses k6chaijs and checks all elements of the API: default export, Guide class instantiation, property writing and reading, method call. In the case of a read-only property, we will get a "TypeError" exception as expected. The value of the read-only "answer" property is of course 42. If any "expect" fails, the k6 run will fail.
The API definition is the primary source for everything, including the documentation. The commands below generate Markdown and HTML API documentation from the API definition. The advantage of this solution is that there is no need to complicate the extension development and build process by using Node.js tools. Internally the HTML documentation is made from the Markdown format. At the moment, the generated documentation is minimalistic, and there is room for improvement in the future. Examples of generated documentation can be found in the examples/faker directory.
In many cases, the API documentation is part of a containing document. For example, the README.md file of the extension's repository contains a chapter on the API documentation. In such cases, the location of the API documentation can be marked with a so-called marker comment. The generation replaces the part between the marker comments with the current API documentation. Instead of the output, the containing document must be specified using the "inject" flag.
And of course there is the case when I want something completely different from the API definition. The API definition can be converted into a JSON data model and processed with other tools. So the API definition remains the single source of truth. External tools can use a JSON parser instead of the complicated TypeScript declaration file parsing.
A few technological details about the tygor follow.
What were the design considerations of tygor? One of the main considerations was to support an API-First approach. That is, first the developer designs (and documents) the JavaScript API of the k6 extension in an interface description language, then the go source code and documentation are generated from this. The de facto interface description language of JavaScript is the TypeScript declaration file. That's why it was chosen. The API changes over time, so it was important to be able to regenerate the output in case of changes. From a convenience point of view, it is good if the implementation is available in a single binary form.
A few bullet points about how tygor works. A more detailed description can be found in the readme file. An API model is built as a first step to generate the go source code and documentation. The API model build was implemented in TypeScript. The TypeScript declaration file is parsed using the TypeScript compiler API. For this purpose, a real TypeScript compiler is embedded in tygor. The embedded TypeScript compiler runs on a built-in JavaScript engine (goja), which may be familiar from k6. The API model is passed as a JSON string from the JavaScript engine to the go code. The go source code is generated from the API model using the Jennifer go source code generator library. The documentation is generated from the API model using the go template engine. The HTML version of the documentation will be made from the Markdown version using the blackfriday library.
Now the live demo follows. I'm going to cheat a little, I'm going to copy code from the previous slides of the presentation. That is, the presentation will be the cheat sheet for the demo. First I create a working directory. cd /tmp mkdir demo demo cd I'm initializing a new go module. go mod init example.com/demo I create the file containing the API definition (first cheat). vi hitchhiker.d.ts I generate the go source code from the API definition. tygor --skeleton hitchhiker.d.ts I will copy the skeleton file, this will be the implementation. cp hitchhiker_skeleton.go hitchhiker.go I will prepare the implementation (second cheat). I download the necessary go modules. go mod tidy I write the integration test (third cheat). vi test.js And finally I run the integration test. xk6 run test.js
That's all I wanted to share with you about the tygor in brief. Thank you for your attention.