Codegen Tutorial
info
For more detailed information about code generation, refer to Code Generation
In this tutorial, we're going to generate code for a simple web application that returns a random pet.
#
StartCreate a new folder to build our project in. Now create a specs
folder to put our specification files. All Sysl code generation projects start from a specification file.
#
ApplicationsCreate a new file called petdemo.sysl
in specs
with the following content:
# backend specificationsimport petstore.yaml as petstore.Petstore
Petdemo "Petdemo": @package="Petdemo" /pet: GET: | Get a random pet Petstore <- GET /pet return ok <: Pet
!type Pet: breed <: string
Create another file called petstore.yaml
with the following content:
openapi: "3.0.3"info: version: 1.0.0 title: Pet Service license: name: MITservers: - url: https://australia-southeast1-innate-rite-238510.cloudfunctions.net/pet-demopaths: /pet: get: summary: Get random pet operationId: getPet tags: - pet responses: "200": description: A random pet content: application/json: schema: $ref: "#/components/schemas/Pet" default: description: unexpected error content: application/json: schema: $ref: "#/components/schemas/Error"components: schemas: Pet: type: string Error: type: object required: - code - message properties: code: type: integer format: int32 message: type: string
#
GenerateFirst, run the bootstrap script using the command below
docker run -it --rm -v `pwd`:/work anzbank/sysl-go:latest specs/petdemo.sysl github.com/anz-bank/sysl-go-demo Petdemo:Petdemo
You should see the initial files generated including Makefile
, go.mod
and codegen.mk
Now run make to generate the skeleton code for your application.
make
Don't forget to run go mod tidy
to generate the go.sum
file
go mod tidy
This can now be committed if you're using a version control system.
#
HandlerBefore you can successfully run your application, you need to add some handler functions for the endpoints you've defined in your specification.
To do this, create the file in the path internal\handlers\pet.go
package handlers
import ( "context" "net/http"
petdemo "github.com/anz-bank/sysl-go-demo/gen/pkg/servers/Petdemo" "github.com/anz-bank/sysl-go-demo/gen/pkg/servers/Petdemo/petstore" "github.com/anz-bank/sysl-go/common")
// GetRandomPetPicListRead reads random pic from downstreamfunc GetRandomPetPicListRead(ctx context.Context, getRandomPetPicListRequest *petdemo.GetPetListRequest, client petdemo.GetPetListClient) (*petdemo.Pet, error) {
// Retrieve the pets (using a fresh set of headers for the request) // This is required because by default Sysl-go reuses inbound headers for downstream requests ctx = common.RequestHeaderToContext(ctx, http.Header{}) reqPetstore := petstore.GetPetListRequest{} pet, err := client.PetstoreGetPetList(ctx, &reqPetstore) if err != nil { return nil, err }
// Return the result return &petdemo.Pet{ Breed: string(*pet), }, nil}
Now we need to register the handler
Add the following line to L21 of cmd/Petdemo/main.go
GetPetList: handlers.GetRandomPetPicListRead,
Great, now try and run it with
go run cmd/Petdemo/main.go
You'll notice that we get the following error message:
configuration is empty
Now we need to add our config file!
#
ConfigAdd the following config file in config/config.yaml
library: log: format: text level: info caller: false
genCode: upstream: contextTimeout: 120s http: basePath: / readTimeout: 120s writeTimeout: 120s common: hostName: "" port: 6060 downstream: contextTimeout: 120s petstore: serviceURL: https://australia-southeast1-innate-rite-238510.cloudfunctions.net/pet-demo clientTimeout: 59s
Great, now you can run the application, passing in the config file
go run cmd/Petdemo/main.go config/config.yaml
Now open your browser to http://localhost:6060/pet
and you should see
Note: The request may timeout the first time since the downstream is a Google Cloud Function. Try a few times to get a successful response
#
Multiple CallsNow let's add another API.
This integrates the Pokemon API, a test API that doesn't require authentication which can be found at https://pokeapi.co/:
openapi: "3.0.0"info: version: 1.0.0 title: PokeAPI Service license: name: MITservers: - url: https://pokeapi.co/api/v2paths: /pokemon/{id}: get: summary: Get a pokemon operationId: getPokemon tags: - pokemon parameters: - name: id description: The identifier for this resource. in: path required: true schema: type: integer responses: "200": description: A pokemon content: application/json: schema: $ref: "#/components/schemas/Pokemon" default: description: unexpected error content: application/json: schema: $ref: "#/components/schemas/Error"components: schemas: Pokemon: type: object properties: id: type: integer description: The identifier for this resource. name: type: string description: The name for this resource. height: type: integer description: The height of this Pokémon in decimetres. Error: type: object required: - code - message properties: code: type: integer format: int32 message: type: string
Add the following import
statement to the top of your Sysl file:
import pokeapi.yaml as pokeapi.PokeAPI
And the following call statement:
PokeAPI <- GET /pokemon/{id}
This declares that we are calling the GET pokemon endpoint on the PokeAPI service.
# import downstream specificationsimport petstore.yaml as petstore.Petstoreimport pokeapi.yaml as pokeapi.PokeAPI
Petdemo "Petdemo": @package="Petdemo" /pet: GET: | Get a random pet Petstore <- GET /pet PokeAPI <- GET /pokemon/{id} return ok <: Pet
!type Pet: breed <: string
Now run make
and some new PokeAPI client code is generated in the gen
folder.
Add the following YAML block to your config.yaml
file:
pokeapi: serviceURL: https://pokeapi.co/api/v2 clientTimeout: 59s
Your resultant config.yaml
file should look like this:
library: log: format: text level: info caller: false
genCode: upstream: contextTimeout: 120s http: basePath: / readTimeout: 120s writeTimeout: 120s common: hostName: "" port: 6060 downstream: contextTimeout: 120s petstore: serviceURL: https://australia-southeast1-innate-rite-238510.cloudfunctions.net/pet-demo clientTimeout: 59s pokeapi: serviceURL: https://pokeapi.co/api/v2 clientTimeout: 59s
Great, now we'll call the PokeAPI service in our handler code.
Let's define a new request type called reqPokemon
.
If you type pokeapi.
your IDE should offer GetPokemonRequest
as an autocomplete option. Let's use the response of the random pet we received as the ID
parameter.
reqPokemon := pokeapi.GetPokemonRequest{ID: int64(len(*pet))}
We can make a call to the endpoint by typing client.
and we'll see the autocomplete option PokeapiGetPokemon
. We simply need to pass in the context
and GetPokemonRequest
object we defined previously.
We'll be appending the result to the Breed
field for now, but we'll fix this up later.
We also need to set the Accept-Encoding
header as sysl-go currently only supports gzip
encoding. Put this before your reqPokemon
definition.
headers := http.Header{} headers.Set("Accept-Encoding", "gzip") ctx = common.RequestHeaderToContext(ctx, headers)
Your pet.go
file should now look like this:
package handlers
import ( "context" "net/http"
petdemo "github.com/anz-bank/sysl-go-demo/gen/pkg/servers/Petdemo" "github.com/anz-bank/sysl-go-demo/gen/pkg/servers/Petdemo/petstore" "github.com/anz-bank/sysl-go-demo/gen/pkg/servers/Petdemo/pokeapi" "github.com/anz-bank/sysl-go/common")
// GetRandomPetPicListRead reads random pic from downstreamfunc GetRandomPetPicListRead(ctx context.Context, getRandomPetPicListRequest *petdemo.GetPetListRequest, client petdemo.GetPetListClient) (*petdemo.Pet, error) {
// Retrieve the pets (using a fresh set of headers for the request) // This is required because by default Sysl-go reuses inbound headers for downstream requests ctx = common.RequestHeaderToContext(ctx, http.Header{}) reqPetstore := petstore.GetPetListRequest{} pet, err := client.PetstoreGetPetList(ctx, &reqPetstore) if err != nil { return nil, err }
// Set the Accept-Encoding header for the PokeapiGetPokemon request // This is required as only gzip encoding is currently supported by Sysl-go headers := http.Header{} headers.Set("Accept-Encoding", "gzip") ctx = common.RequestHeaderToContext(ctx, headers)
// Retrieve the pokemon reqPokemon := pokeapi.GetPokemonRequest{ID: int64(len(*pet))} pokemon, err := client.PokeapiGetPokemon(ctx, &reqPokemon) if err != nil { return nil, err }
// Return the result return &petdemo.Pet{ Breed: string(*pet) + " " + *pokemon.Name, }, nil}
Great, now run go run cmd/Petdemo/main.go config/config.yaml
and navigate your browser to http://localhost:6060/pet
.
You should see the name of a pokemon appended to the random pet. e.g
{ "breed": "guinea pig caterpie" }
Let's put this in its own field now.
Add the pokemon
field to the response type.
Your Sysl file should now look like the following:
# import downstream specificationsimport petstore.yaml as petstore.Petstoreimport pokeapi.yaml as pokeapi.PokeAPI
Petdemo "Petdemo": @package="Petdemo" /pet: GET: | Get a random pet Petstore <- GET /pet PokeAPI <- GET /pokemon/{id} return ok <: Pet
!type Pet: breed <: string pokemon <: string
Now run make
and modify the return statement in pet.go
so that we return the Pokemon in a separate field.
package handlers
import ( "context" "net/http"
petdemo "github.com/anz-bank/sysl-go-demo/gen/pkg/servers/Petdemo" "github.com/anz-bank/sysl-go-demo/gen/pkg/servers/Petdemo/petstore" "github.com/anz-bank/sysl-go-demo/gen/pkg/servers/Petdemo/pokeapi" "github.com/anz-bank/sysl-go/common")
// GetRandomPetPicListRead reads random pic from downstreamfunc GetRandomPetPicListRead(ctx context.Context, getRandomPetPicListRequest *petdemo.GetPetListRequest, client petdemo.GetPetListClient) (*petdemo.Pet, error) {
// Retrieve the pets (using a fresh set of headers for the request) // This is required because by default Sysl-go reuses inbound headers for downstream requests ctx = common.RequestHeaderToContext(ctx, http.Header{}) reqPetstore := petstore.GetPetListRequest{} pet, err := client.PetstoreGetPetList(ctx, &reqPetstore) if err != nil { return nil, err }
// Set the Accept-Encoding header for the PokeapiGetPokemon request // This is required as only gzip encoding is currently supported by Sysl-go headers := http.Header{} headers.Set("Accept-Encoding", "gzip") ctx = common.RequestHeaderToContext(ctx, headers)
// Retrieve the pokemon reqPokemon := pokeapi.GetPokemonRequest{ID: int64(len(*pet))} pokemon, err := client.PokeapiGetPokemon(ctx, &reqPokemon) if err != nil { return nil, err }
// Return the result return &petdemo.Pet{ Breed: string(*pet), Pokemon: *pokemon.Name, }, nil}
Great, now run go run cmd/Petdemo/main.go config/config.yaml
and navigate your browser to http://localhost:6060/pet
You should now see something similar to the following response
{ "breed": "guinea pig", "pokemon": "caterpie" }
#
Conditional LogicLet's add some conditional logic to our specification file to specify that we only call the PokeAPI when the length of the response is less than 50.
Your petdemo.sysl
file should look like the following:
# import downstream specificationsimport petstore.yaml as petstore.Petstoreimport pokeapi.yaml as pokeapi.PokeAPI
Petdemo "Petdemo": @package="Petdemo" /pet: GET: | Get a random pet Petstore <- GET /pet if len(Pet) < 50: PokeAPI <- GET /pokemon/{id} return ok <: Pet
!type Pet: breed <: string pokemon <: string
Run make
and notice how the generated code hasn't changed. This is because Sysl doesn't generate business logic, this must be done in the handler code written by you.
However, the conditional logic in the Sysl file does show up in the diagrams generated for you.
Run the command below and open your browser to localhost:6900
. We're interested in the Petdemo
service so navigate to there and expand the sequence diagram for the GET /pet
endpoint.
docker run -p 6900:6900 -v $(pwd):/usr:ro anzbank/sysl-catalog:v1.4.199 run --serve -v usr/specs/petdemo.sysl
You can see here that the sequence diagram shows the conditional logic defined in your Sysl file. These automatically-generated diagrams help you to easily understand the behavior of your application.
#
HeadersYou may have noticed a curious comment in some of the snippets above:
// Retrieve the pets (using a fresh set of headers for the request)// This is required because by default Sysl-go reuses inbound headers for downstream requestsctx = common.RequestHeaderToContext(ctx, http.Header{})reqPetstore := petstore.GetPetListRequest{}pet, err := client.PetstoreGetPetList(ctx, &reqPetstore)
You'll notice that the client.PetstoreGetPetList
method takes as arguments a context.Context
and a request object but that the request object does not have any knowledge of HTTP headers.
This is a deliberate decision designed to keep downstream request method signatures free from implementation details like HTTP headers and bodies.
However, in order to support HTTP headers as an implementation detail where required, headers can be passed through the context by calling the common.RequestHeaderToContext
method.
The unfortunate outcome of this approach is that HTTP headers are also passed into handlers using the same method and so, by default, inbound HTTP headers are passed into downstream requests.
This is a known, legacy implementation detail that we aim to address soon but in the meantime we encourage that a fresh set of HTTP headers are set against the context before all downstream HTTP requests are made.
#
Next stepsTODO: We'll be adding more instructions soon to use the response we received to fetch an image of a random pet.