codegen

package
v0.14.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 24, 2024 License: Apache-2.0 Imports: 11 Imported by: 0

README

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.yml file is the openAPI spec, and the golden directory 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.go exists, myfile.goref will 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 reference regeneration rule & manually compare the original file to the ref file
  • Currently just supports creation of cmd, pkg/api & pkg/controller packages, 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
  1. 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
  1. 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
  1. 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

  1. Make a folder for your templates
mkdir myTemplates
  1. Copy the contents of template/src into myTemplates
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)

  1. Remove the cmd and pkg/api folders
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

  1. Update codegen.yml with templateDirectory
contract: ./contract.yml
repositoryRootPath: github.com/repo_owner/testservice
templateDirectory: myTemplate
projectName: testservice
  1. Run lanai-cli codegen -o ./, and the pkg folder 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.go for 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 (see Development/Generators). Supported rules include:

    • regeneration

    Matching is done via filepath.Match, so check what kind of patterns you can use there.

    In the above example, this would apply the reference regeneration rule to pkg/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 project will be generated once
  • Templates starting with api will 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 version will be generated once per version found in the API (e.g. /idm/api/v8/roles/list will generate a file for the v8 version)
    • Tracks the version and all paths that are a part of that api version
  • Templates starting with delete and 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 line package myPackage)
  • Any other .tmpl files 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 regexes config 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 part path
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)

Documentation

Index

Constants

View Source
const (
	CommandName = "codegen"
)
View Source
const DefaultTemplateRoot = "template/src"

Variables

View Source
var (
	Cmd = &cobra.Command{
		Use:                CommandName,
		Short:              "Given openapi contract, generate controllers/structs",
		FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true},
		RunE:               Run,
	}
	Args = Arguments{}
)
View Source
var DefaultConfigV2 = ConfigV2{
	Components: ComponentsV2{
		Security: SecurityV2{
			Authentication: AuthenticationV2{Method: generator.AuthOAuth2},
			Access:         AccessV2{Preset: generator.AccessPresetFreestyle},
		},
	},
	Regen: RegenerationV2{
		Default: RegenMode(generator.RegenModeIgnore),
	},
}
View Source
var DefaultTemplateFS embed.FS
View Source
var DefaultVersionedConfig = VersionedConfig{
	Version:  "v1",
	ConfigV2: DefaultConfigV2,
}

Functions

func GenerateWithConfig

func GenerateWithConfig(ctx context.Context, cfg *ConfigV2) error

func GenerateWithConfigPath

func GenerateWithConfigPath(ctx context.Context, configPath string) error

func Run

func Run(cmd *cobra.Command, _ []string) error

Types

type AccessV2

type AccessV2 struct {
	Preset generator.AccessPreset `json:"preset"`
}

type Arguments

type Arguments struct {
	Config string `flag:"config,c" desc:"Configuration file, if not defined will default to codegen.yml"`
}

type AuthenticationV2

type AuthenticationV2 struct {
	Method generator.AuthenticationMethod `json:"method"`
}

type ComponentsV2

type ComponentsV2 struct {
	Contract ContractV2 `json:"contract"`
	Security SecurityV2 `json:"security"`
}

func (*ComponentsV2) ToOption

func (c *ComponentsV2) ToOption() generator.Options

type Config

type Config struct {
	Contract           string            `json:"contract"`
	ProjectName        string            `json:"projectName"`
	TemplateDirectory  string            `json:"templateDirectory"`
	RepositoryRootPath string            `json:"repositoryRootPath"`
	Regeneration       Regeneration      `json:"regeneration"`
	Regexes            map[string]string `json:"regexes"`
}

func (Config) ToV2

func (c Config) ToV2() *ConfigV2

type ConfigV2

type ConfigV2 struct {
	Project    ProjectV2      `json:"project"`
	Templates  TemplatesV2    `json:"templates"`
	Components ComponentsV2   `json:"components"`
	Regen      RegenerationV2 `json:"regen"`
}

func (ConfigV2) ToOptions

func (c ConfigV2) ToOptions() []generator.Options

type ConfigVersion

type ConfigVersion string
const (
	VersionUnknown ConfigVersion = ``
	Version1       ConfigVersion = `v1`
	Version2       ConfigVersion = `v2`
)

type ContractNamingV2

type ContractNamingV2 struct {
	RegExps map[string]string `json:"regular-expressions"`
}

type ContractV2

type ContractV2 struct {
	Path   string           `json:"path"`
	Naming ContractNamingV2 `json:"naming"`
}

type ProjectV2

type ProjectV2 struct {
	// Name service name
	Name string `json:"name"`
	// Module golang module
	Module string `json:"module"`
	// Port
	Port int `json:"port"`
	// ContextPath golang module
	ContextPath string `json:"context-path"`
	// Description golang module
	Description string `json:"description"`
}

func (*ProjectV2) ToOption

func (p *ProjectV2) ToOption() generator.Options

type RegenMode

type RegenMode generator.RegenMode

type RegenRule

type RegenRule struct {
	// Pattern wildcard pattern of output file path
	Pattern string `json:"pattern"`
	// Mode regeneration mode on matched output files in case of changes. (ignore, overwrite, reference, etc.)
	Mode RegenMode `json:"mode"`
}

type RegenRules

type RegenRules []RegenRule

type Regeneration

type Regeneration struct {
	Default string            `json:"default"`
	Rules   map[string]string `json:"rules"`
}

type RegenerationV2

type RegenerationV2 struct {
	Default RegenMode  `json:"default"`
	Rules   RegenRules `json:"rules"`
}

func (RegenerationV2) ToOption

func (r RegenerationV2) ToOption() func(*generator.Option)

type SecurityV2

type SecurityV2 struct {
	Authentication AuthenticationV2 `json:"authn"`
	Access         AccessV2         `json:"access"`
}

type TemplatesV2

type TemplatesV2 struct {
	Path string `json:"path"`
}

type VersionedConfig

type VersionedConfig struct {
	Version ConfigVersion `json:"version"`
	Config
	ConfigV2
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL