climate

package module
v0.0.0-...-03e4949 Latest Latest
Warning

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

Go to latest
Published: Feb 17, 2026 License: MIT Imports: 12 Imported by: 0

README

climate

Go Report Card CI Status

Read the detailed blogpost!

Go is a fantastic language to build CLI tooling, specially the ones for interacting with an API server. <your tool>ctl anyone? But if you're tired of building bespoke CLIs everytime or think that the swagger codegen isn't just good enough or don't quite subscribe to the idea of codegen in general (like me!), look no further.

What if you can influence the CLI behaviour from the server? This enables you to bootstrap your cobra or urfave/cli/v3 CLI tooling from an OpenAPI spec. Checkout Wendy as an example of a full CLI project made using climate.

Getting started

Rationale

climate allows the server to influence the CLI behaviour by using OpenAPI's extensions. It encourages spec-first practices thereby keeping both users and maintenance manageable. It does just enough to handle the spec and nothing more.

Overall, the way it works:

  • Each operation is converted to a Cobra or urfave/cli command
  • Each parameter is converted to a flag with its corresponding type
  • As of now, request bodies are a flag and treated as a string regardless of MIME type. Name defaults to climate-data unless specified via x-cli-name. All subject to change
  • The provided handlers are attached to each command, grouped and attached to the rootCmd

Influenced by some of the ideas behind restish it uses the following extensions as of now:

  • x-cli-aliases: A list of strings which would be used as the alternate names for an operation
  • x-cli-group: A string to allow grouping subcommands together. All operations in the same group would become subcommands in that group name
  • x-cli-hidden: A boolean to hide the operation from the CLI menu. Same behaviour as a command hide: it's present and expects a handler
  • x-cli-ignored: A boolean to tell climate to omit the operation completely
  • x-cli-name: A string to specify a different name. Applies to operations and request bodies as of now
Ideally support:
  • more of the OpenAPI types and their checks. eg arrays, enums, objects, multi types etc
  • type checking request bodies of certain MIME types eg, application/json
  • better handling of request bodies eg, providing a stdin or a curl like notation for a file @payload.json etc.
  • more CLI libs?
Installation
go get github.com/lispyclouds/climate
Usage

Given an OpenAPI spec like api.yaml

Load the spec:

model, err := climate.LoadFileV3("api.yaml") // or climate.LoadV3 with []byte

Define a root command:

// Cobra
rootCmd := &cobra.Command{
	Use:   "calc",
	Short: "My Calc",
	Long:  "My Calc powered by OpenAPI",
}

// urfave/cli
rootCmd := &cli.Command{
	Name:  "calc",
	Usage: "My Calc",
}

Define one or more handler functions of the following signature:

// Cobra
func handler(opts *cobra.Command, args []string, data climate.HandlerData) error {
	slog.Info("called!", "data", fmt.Sprintf("%+v", data))
	err := doSomethingUseful(data)

	return err
}

// urfave/cli
func handler(opts *cli.Command, args []string, data climate.HandlerData) error {
	slog.Info("called!", "data", fmt.Sprintf("%+v", data))
	err := doSomethingUseful(data)

	return err
}
Handler Data

(Feedback welcome to make this better!)

As of now, each handler is called with the command it was invoked with, the args and an extra climate.HandlerData, more info here

This can be used to query the params from the command mostly in a type safe manner:

// to get all the int path params
for _, param := range data.PathParams {
	if param.Type == climate.Integer {
		// Cobra
		value, _ := opts.Flags().GetInt(param.Name)

		// urfave/cli
		value, _ := opts.Int(param.Name)
	}
}

Define the handlers for the necessary operations. These map to the operationId field of each operation:

// Cobra
handlers := map[string]HandlerCobra{
	"AddGet":      handler,
	"AddPost":     handler,
	"HealthCheck": handler,
	"GetInfo":     handler,
}

// urfave/cli
handlers := map[string]HandlerUrfaveCliV3{
	"AddGet":      handler,
	"AddPost":     handler,
	"HealthCheck": handler,
	"GetInfo":     handler,
}

Bootstrap the root command:

// Cobra
err := climate.BootstrapV3Cobra(rootCmd, *model, handlers)

// urfave/cli
err := climate.BootstrapV3UrfaveCliV3(rootCmd, *model, handlers)

Continue adding more commands and/or execute:

// add more commands not from the spec

// Cobra
rootCmd.Execute()

// urfave/cli
rootCmd.Run(context.TODO(), os.Args)

Sample output using Cobra:

$ go run main.go --help
My Calc powered by OpenAPI

Usage:
  calc [command]

Available Commands:
  completion  Generate the autocompletion script for the specified shell
  help        Help about any command
  info        Operations on info
  ops         Operations on ops
  ping        Returns Ok if all is well

Flags:
  -h, --help   help for calc

Use "calc [command] --help" for more information about a command.

$ go run main.go ops --help
Operations on ops

Usage:
  calc ops [command]

Available Commands:
  add-get     Adds two numbers
  add-post    Adds two numbers via POST

Flags:
  -h, --help   help for ops

Use "calc ops [command] --help" for more information about a command.

$ go run main.go ops add-get --help
Adds two numbers

Usage:
  calc ops add-get [flags]

Aliases:
  add-get, ag

Flags:
  -h, --help     help for add-get
      --n1 int   The first number
      --n2 int   The second number

$ go run main.go ops add-get --n1 1 --n2 foo
Error: invalid argument "foo" for "--n2" flag: strconv.ParseInt: parsing "foo": invalid syntax
Usage:
  calc ops add-get [flags]

Aliases:
  add-get, ag

Flags:
  -h, --help     help for add-get
      --n1 int   The first number
      --n2 int   The second number

$ go run main.go ops add-get --n1 1 --n2 2
2024/12/14 12:53:32 INFO called! data="{Method:get Path:/add/{n1}/{n2}}"

License

Copyright © 2024- Rahul De

Distributed under the MIT License. See LICENSE.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BootstrapV3 deprecated

func BootstrapV3(rootCmd *cobra.Command, model libopenapi.DocumentModel[v3.Document], handlers map[string]Handler) error

Bootstraps a cobra.Command with the loaded model and a handler map

Deprecated: Will be kept for backwards compatibility. Use BootstrapV3Cobra instead.

func BootstrapV3Cobra

func BootstrapV3Cobra(rootCmd *cobra.Command, model libopenapi.DocumentModel[v3.Document], handlers map[string]HandlerCobra) error

Bootstraps a cobra.Command with the loaded model and a handler map

func BootstrapV3UrfaveCliV3

func BootstrapV3UrfaveCliV3(rootCmd *cli.Command, model libopenapi.DocumentModel[v3.Document], handlers map[string]HandlerUrfaveCliV3) error

Bootstraps a cli.Command with the loaded model and a handler map

func LoadFileV3

func LoadFileV3(path string) (*libopenapi.DocumentModel[v3.Document], error)

Loads and verifies an OpenAPI spec from a file path

func LoadV3

func LoadV3(data []byte) (*libopenapi.DocumentModel[v3.Document], error)

Loads and verifies an OpenAPI spec frpm an array of bytes

Types

type Handler deprecated

type Handler = HandlerCobra

Deprecated: Use HandlerCobra instead

type HandlerCobra

type HandlerCobra func(opts *cobra.Command, args []string, data HandlerData) error

type HandlerData

type HandlerData struct {
	Method           string      // the HTTP method
	Path             string      // the path with the path params filled in
	PathParams       []ParamMeta // List of path params
	QueryParams      []ParamMeta // List of query params
	HeaderParams     []ParamMeta // List of header params
	CookieParams     []ParamMeta // List of cookie params
	RequestBodyParam *ParamMeta  // The optional request body
}

Data passed into each handler

type HandlerUrfaveCliV3

type HandlerUrfaveCliV3 func(opts *cli.Command, args []string, data HandlerData) error

type OpenAPIType

type OpenAPIType string

Currently supported OpenAPI types

const (
	String  OpenAPIType = "string"
	Number  OpenAPIType = "number"
	Integer OpenAPIType = "integer"
	Boolean OpenAPIType = "boolean"
)

type ParamMeta

type ParamMeta struct {
	Name string
	Type OpenAPIType
}

Metadata for all parameters

Jump to

Keyboard shortcuts

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