huma icon indicating copy to clipboard operation
huma copied to clipboard

Any way to create a CLI out of the web server?

Open ashishb opened this issue 1 year ago • 5 comments

Thanks for this awesome package. Is there a way to convert my webserver into an offline CLI as well using huma?

ashishb avatar Sep 20 '24 19:09 ashishb

@ashishb thanks! I'm glad you like it :smile: What are you hoping to do with your CLI? If you use the humacli package you can easily add extra commands, for example the tutorial shows how you can create a command to dump the OpenAPI document without running a server:

  • https://huma.rocks/tutorial/client-sdks/#add-an-openapi-command
  • https://huma.rocks/features/cli/#custom-commands

If you are looking to have a CLI that calls into a running web server, take a look at Restish

  • https://huma.rocks/tutorial/cli-client/
  • https://rest.sh/

danielgtaylor avatar Oct 08 '24 23:10 danielgtaylor

@danielgtaylor I have a set of API calls generated via huma and I want to automatically convert it into CLI

ashishb avatar Oct 14 '24 02:10 ashishb

@ashishb did you read the linked docs? I think they will show you how to do what you want. Let me know if you have specific questions!

danielgtaylor avatar Oct 17 '24 16:10 danielgtaylor

@danielgtaylor I did look at https://huma.rocks/tutorial/cli-client/ I don't need a client. I want to convert the server to the CLI as-is, so, I can run the server as a CLI tool.

ashishb avatar Oct 17 '24 19:10 ashishb

@ashishb you can use the custom commands docs I linked to if you want CLI commands for the server itself. Here's a modified hello world example:

package main

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/danielgtaylor/huma/v2"
	"github.com/danielgtaylor/huma/v2/adapters/humachi"
	"github.com/danielgtaylor/huma/v2/humacli"
	"github.com/danielgtaylor/huma/v2/humatest"
	"github.com/go-chi/chi/v5"
	"github.com/spf13/cobra"

	_ "github.com/danielgtaylor/huma/v2/formats/cbor"
)

// Options for the CLI.
type Options struct {
	Port int `help:"Port to listen on" short:"p" default:"8888"`
}

// GreetingInput represents the greeting operation request.
type GreetingInput struct {
	Name string `path:"name" maxLength:"30" example:"world" doc:"Name to greet"`
}

// GreetingOutput represents the greeting operation response.
type GreetingOutput struct {
	Body struct {
		Message string `json:"message" example:"Hello, world!" doc:"Greeting message"`
	}
}

func main() {
	var router *chi.Mux

	// Create a CLI app which takes a port option.
	cli := humacli.New(func(hooks humacli.Hooks, options *Options) {
		// Create a new router & API
		router = chi.NewMux()
		api := humachi.New(router, huma.DefaultConfig("My API", "1.0.0"))

		// Register GET /greeting/{name}
		huma.Register(api, huma.Operation{
			OperationID: "get-greeting",
			Summary:     "Get a greeting",
			Method:      http.MethodGet,
			Path:        "/greeting/{name}",
		}, func(ctx context.Context, input *GreetingInput) (*GreetingOutput, error) {
			resp := &GreetingOutput{}
			resp.Body.Message = fmt.Sprintf("Hello, %s!", input.Name)
			return resp, nil
		})

		// Tell the CLI how to start your router.
		hooks.OnStart(func() {
			http.ListenAndServe(fmt.Sprintf(":%d", options.Port), router)
		})
	})

	cli.Root().AddCommand(&cobra.Command{
		Use:   "get-greeting",
		Short: "Greet someone",
		Args:  cobra.ExactArgs(1),
		Run: func(cmd *cobra.Command, args []string) {
			req := httptest.NewRequest(http.MethodGet, "/greeting/"+args[0], nil)
			resp := httptest.NewRecorder()
			router.ServeHTTP(resp, req)
			humatest.PrintResponse(resp.Result())
		},
	})

	// Run the CLI. When passed no commands, it starts the server.
	cli.Run()
}

You can run it like:

$ go run . get-greeting bob
HTTP/1.1 200 OK
Connection: close
Content-Type: application/json
Link: </schemas/GreetingOutputBody.json>; rel="describedBy"

{
  "$schema": "https://example.com/schemas/GreetingOutputBody.json",
  "message": "Hello, bob!"
}

You can write whatever commands you want this way and simulate a request to the server. If you don't want the overhead of HTTP request/response stuff, you can instantiate your handler's request struct manually and call it directly, then handle the response struct however you want, or factor out common code that both the CLI operation and handler can call. Hopefully this helps!

danielgtaylor avatar Oct 17 '24 21:10 danielgtaylor