go-lanai Code Generator
Codegen will read an openAPI contract, and generate structs & controllers for your go-lanai project.
What it will do:
- Reads an openAPI spec and:
- generates structs for each component, request & response defined
- generate struct bindings from a field's required-ness, input validation and min/max fields
 
- generates controllers for each path, with function stubs for each operation
- generates package.go files for the created packages, as well as a go.mod for the project
- See example generated files
- The test.ymlfile is the openAPI spec, and thegoldendirectory is the generated output from that spec
 
 
- Contains a default set of templates, and support the user to define their own
- Regeneration rules can be defined for when a file to be generated already exists:
- overwriting - new file replaces old file
- ignoring - new file is not written
- reference - new file is generated beside the old one (e.g. if myfile.goexists,myfile.gorefwill be generated), for manual merge
 
What it won't do:
- Currently, it lacks capability to "intelligently" automatically resolve user changes with contract changes when regenerating
code
- Recommended to use the referenceregeneration rule & manually compare the original file to thereffile
 
- Currently just supports creation of cmd,pkg/api&pkg/controllerpackages, whcih corresponds to the REST controller
layer of a web application written based on go-lanai's web module. If the project needs any other go-lanai frameworks,
they will need to be added manually
Running
To generate code, you will need an openAPI spec and a codegen.yml file (see Configuration). The following command will
generate the project source code to ./dist:
lanai-cli codegen
You can add arguments, providing your own myConfig.yml, and specifying your own output folder path/to/output/folder:
lanai-cli codegen -c myConfig.yml -o path/to/output/folder
Examples
Generation
To see exactly what is generated by the generator, see the
test golden files when given an openAPI contract
Usage
- Make an openAPI contract
contract.yml
openapi: 3.0.0
info:
  title: 'Test Contract'
  version: '1'
  termsOfService: 'https://www.cisco.com'
  license:
    name: 'MIT'
paths:
  '/idm/api/v1/hello':
    get:
      summary: Get Hello API
      operationId: GetHello
      parameters:
        - name: scope
          in: path
          required: true
          schema:
            format: "^[a-zA-Z0-5-_=]{1,256}$"
            type: string
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MyObject'
  '/idm/api/v2/hi':
    get:
      summary: Get Hi API
      operationId: GetHi
      parameters:
        - name: scope
          in: path
          required: true
          schema:
            format: "^[a-zA-Z0-5-_=]{1,256}$"
            type: string
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MyObject'
components:
  schemas:
    MyObject:
      type: object
      properties:
        id:
          type: string
        enabled:
          type: object
          properties:
            inner:
              type: string
- Make a codegen.yml
Notes: If contract/templateDirectory are relative paths, they must be relative to the location of this config file.
contract: ./contract.yml
repositoryRootPath: github.com/repo_owner/testservice
projectName: testservice
- Run lanai-cli codegen -o ./Files will be generated to your directory:
├── cmd
│   ├── testservice
│   │   └── main.go
│   └── testservice-migrate
│       └── migrate.go
├── codegen.yml
├── contract.yml
├── go.mod
├── go.sum
└── pkg
    ├── api
    │   ├── common.go
    │   ├── v1
    │   │   └── hello.go
    │   └── v2
    │       └── hi.go
    └── controller
        ├── package.go
        ├── v1
        │   ├── hello.go
        │   └── package.go
        └── v2
            ├── hi.go
            └── package.go
Using Your Own Templates
By default, codegen uses the set of templates defined in template/src,
mirroring the file structure of a typical go-lanai web service.
If you just want to generate the pkg/controller files
- Make a folder for your templates
mkdir myTemplates
- Copy the contents of template/srcintomyTemplates
myTemplates
├── cmd
│   ├── @ProjectName@
│   │   └── project.main.go.tmpl
│   └── @ProjectName@-migrate
│       └── project.migrate.go.tmpl
├── delete.empty.tmpl
├── pkg
│   ├── api
│   │   ├── @Version@
│   │   │   ├── api-struct.requestresponse.go.tmpl
│   │   │   ├── requeststructs.tmpl
│   │   │   └── responsestructs.tmpl
│   │   ├── project.common.go.tmpl
│   │   └── structs.tmpl
│   └── controller
│       ├── @Version@
│       │   ├── api.controllers.go.tmpl
│       │   ├── controller.tmpl
│       │   └── version.package.go.tmpl
│       └── project.package.go.tmpl
└── project.go.mod.tmpl
(For more info about the significance about the different prefixes, see Development/Generators)
- Remove the cmdandpkg/apifolders
rm -rf myTemplates/cmd myTemplates/pkg/api
├── codegen.yml
├── contract.yml
├── go.mod
└── myTemplates
    ├── delete.empty.tmpl
    ├── pkg
    │   └── controller
    │       ├── @Version@
    │       │   ├── api.controllers.go.tmpl
    │       │   ├── controller.tmpl
    │       │   └── version.package.go.tmpl
    │       └── project.package.go.tmpl
    └── project.go.mod.tmpl
- Update codegen.ymlwithtemplateDirectory
contract: ./contract.yml
repositoryRootPath: github.com/repo_owner/testservice
templateDirectory: myTemplate
projectName: testservice
- Run lanai-cli codegen -o ./, and thepkgfolder will be generated
├── codegen.yml
├── contract.yml
├── go.mod
├── go.sum
├── myTemplates
└── pkg
    └── controller
        ├── package.go
        ├── v1
        │   ├── hello.go
        │   └── package.go
        └── v2
            ├── hi.go
            └── package.go
Running Codegen in a Repository that has Existing Files
Currently, there's no automatic method of resolving changes when regenerating existing files, so there are a few regeneration rules to assist
manual resolving.
Take this project, with some unique change to hello.go
└── pkg
│    ...
    └── controller
        ├── package.go
        ├── v1
        │   ├── hello.go* // contains user changes
        │   └── package.go
In codegen.yml, if you set regeneration  to overwrite & run:
contract: ./contract.yml
repositoryRootPath: github.com/repo_owner/testservice
projectName: testservice
regeneration:
  default: overwrite
It will regenerate the files & blow away any user changes. This is the default behavior:
└── pkg
│    ...
    └── controller
        ├── package.go
        ├── v1
        │   ├── hello.go // user changes blown away
        │   └── package.go
Change it to ignore & run
regeneration:
  default: ignore 
It won't modify any existing old files:
└── pkg
│    ...
    └── controller
        ├── package.go
        ├── v1
        │   ├── hello.go* // file left alone
        │   └── package.go
If you change it to reference
regeneration:
  default: overwrite
It'll generate a new file next to the original with a ref suffix:
└── pkg
│    ...
    └── controller
        ├── package.go
        ├── v1
        │   ├── hello.go* // file left alone
        │   ├── hello.goref // new version created next to original for comparison
        │   └── package.go
Configuration
Here is an example of a configuration yml file:
contract: ./contract.yml
templateDirectory: template/src
repositoryRootPath: github.com/repo_owner/testservice
projectName: testservice
regeneration:
  default: overwrite
  # Applies specific rules to files matching these patterns
  rules:
    "pkg/controller/*/*": reference #overwrite | ignore | reference
regexes:
  testRegex: "^[a-zA-Z0-5-_=]{1,256}$"
codegen.yml supports the following flags:
- contract- path to the openAPI contract. Supports OpenAPI 3.0, see an example here.
- projectName- e.g.- testservice
- repositoryRootPath- eg.- github.com/repo_owner/testservice
- templateDirectory- if defined, the code generator will use that directory to get its templates.
- regeneration- policies for generating files that already exist.
This table describes the behaviors of each rule when you try to regenerate on- myFile.gofor each of the rules:- 
- v2 would be a freshly generated file based on the contract at the time it was run - not containing any user changes in v1
 
| Regeneration Rule | Before | After | 
| overwrite(default) | myFile.go (v1) | myFile.go (v2) | 
| ignore | myFile.go (v1) | myFile.go (v1) | 
| reference | myFile.go (v1) | myFile.go (v1), myFile.goref(v2) | 
- 
regexes- a string-string array, label any regexes that appear in the contract so they can be registered by
go-lanai's validator. This field is optional - any unlabelled fields will be given a generated name
 
- 
rules- each entry of this map must a file pattern, to be applied to any generated files that match the pattern.
Any rules defined here will overwrite the global rule for relevant files (seeDevelopment/Generators). Supported rules include:
 Matching is done via filepath.Match, so check what kind of patterns you can use there. In the above example, this would apply the referenceregeneration rule topkg/controller/v1/package.go
 
Development
Templates
In most use-cases, the default set of templates defined in template/src should be good enough for code generation,
but you can use your own template set through the templateDirectory field in the config.
The file structure of the directory used will reflect the file structure of the generated code.
If the file path is nested in @ symbols, those will be replaced by the appropriate information from the generator (see below).
Generators
The project has different generators that behave differently based on the prefix of the templates in the filesystem.
- Templates starting with projectwill be generated once
- Templates starting with apiwill be generated once per API path in the openAPI contract
- Tracks the name and data of the path, as well as the appropriate API version it belongs to.
 
- Templates starting with versionwill be generated once per version found in the API
(e.g. /idm/api/v8/roles/list will generate a file for thev8version)
- Tracks the version and all paths that are a part of that api version
 
- Templates starting with deleteand containing a regex will run at the end and delete any files matching that regex.
A use case would be if the generator made an excess file that doesn't contain useful information (i.e just the linepackage myPackage)
- Any other .tmplfiles won't generate anything and any templates defined can be used by the other ones
Writing Your Own Templates
This project uses Golang's text templating engine, and provides various models & functions
to serve as helpers.
Note that some of these functions are pretty specific to the use-case of the default templates, so YMMV on their usefulness
when writing your own.
Random Helpers
| Function Name | Usage | Comments | 
| args | args <...interface > | Helper to provide arguments to a template block. Arguments can be accessed with index . <arg index > | 
| increment | increment <int> | Given a number, returns that number + 1 | 
| log | log <interface> | logs your message in the console | 
| listContains | listContains <haystack []string>, <needle string> | Returns true if the needle is in the haystack | 
| derefBoolPtr | derefBoolPtr <bool*> | converts a boolean pointer and returns the underlying bool | 
| Function Name | Usage | Comments | 
| regex | regex <openapi3.Schema> | Returns a regex object, providing a regex name (i.e "myRegex", generated or defined from the regexesconfig field) and the value (e.g "^[a-zA-Z0-7-_=]{1,256}$") | 
| registerRegex | registerRegex <openapi3.Schema> | Registers the regex from the schema internally & returns the regex's name. If the regex has already been registered, returns nothing | 
| Function Name | Usage | Comments | 
| toTitle | toTitle <string> | Applies title cases ( myStruct->MyStruct) | 
| concat | concat <...string> | Joins all the provided strings together | 
| toLower | toLower <string> | Sets string to all lowercase MyStruct->mystruct | 
| basePath | basePath <string> | Given a path my/cool/path, returns the base partpath | 
| hasPrefix | hasPrefix <string> | Calls strings.HasPrefix on the input | 
| replaceDashes | replaceDashes <string> | Replaces any dashes in string with underscores | 
| Function Name | Usage | Comments | 
| versionList | versionList <openapi3.Paths> | Given all the paths, returns a list of all the versions (e.g v1,v2, etc) | 
| mappingName | mappingName <path - string> <operation - string> | '/my/api/v1/testpath/{scope}'+ GET operation ->testpath-scope-get | 
| mappingPath | mappingPath <string> | '/my/api/v1/testpath/{scope}'->/api/v1/testpath/:scope | 
Constructors:
| Function Name | Usage | Comments | 
| property | property <data - interface{}, name - string, currentPkg - string, prefix ...string> |  | 
| operation | operation <data - *openapi3.Operation, string> | If the operation lacks an OperationID, use the second argument to assign a name | 
| schema | schema <string, data - *openapi3.SchemaRef> |  | 
| Function Name | Usage | Comments | 
| shouldHavePointer | shouldHavePointer <interface{}, isRequired bool> | Check for if this struct property should be a pointer | 
| structTags | structTag <representation.Property> | Returns the struct tags of the property, if the property is in the list of requiredParams, a required tag will be added | 
| requiredList | requiredList <*SchemaRef or *Parameter> | Returns a list of any required parameters in this object | 
| containsSingularRef | containsSingularRef | Returns true if the object doesn't contain anything except one ref | 
| defaultNameFromPath | defaultNameFromPath <string> | /my/api/v1/testpath/{scope}->TestpathScope | 
| registerStruct | registerStruct <schemaName string, packageName string> | Registers a struct & it's package name for the purposes of importing | 
| structLocation | structLocation <structName string> | Returns the package that the struct is a part of | 
| importsUsedByPath | importsUsedByPath <openapi3.PathItem> | Returns a list of imports (i.e where structs are located) that the path is expected to use | 
| isEmpty | isEmpty | Returns true if the object doesn't actually contain any fields (i.e parameters, or anything in the response) |