Skip to content

Commit

Permalink
KTOR-7748 Generate project by given OpenAPI spec
Browse files Browse the repository at this point in the history
  • Loading branch information
Stexxe committed Nov 18, 2024
1 parent 87f9b31 commit 80a1f90
Show file tree
Hide file tree
Showing 20 changed files with 446 additions and 107 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,18 @@ To create a new project in the interactive mode, simply use the `new` command wi
ktor new
```

## Generate project from OpenAPI specification

To generate a project in the current directory from the given [OpenAPI specification](https://swagger.io/specification/), use an `openapi` command:
```shell
ktor openapi petstore.yaml
```

You can specify a different output directory with the `-o` or `--output` flag:
```shell
ktor openapi -o path/to/project petstore.yaml
```

## Get the version

To get the version of the tool, use the `--version` flag or the `version` command:
Expand Down
64 changes: 41 additions & 23 deletions cmd/ktor/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
_ "embed"
"errors"
"fmt"
"github.com/ktorio/ktor-cli/internal/app/cli"
"github.com/ktorio/ktor-cli/internal/app/cli/command"
Expand Down Expand Up @@ -59,25 +60,25 @@ func main() {

ctx := context.WithValue(context.Background(), "user-agent", fmt.Sprintf("KtorCLI/%s", getVersion()))

client := &http.Client{
Transport: &http.Transport{
DialContext: func(_ context.Context, network, addr string) (net.Conn, error) {
return net.DialTimeout(network, addr, 5*time.Second)
},
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
},
}

switch args.Command {
case cli.VersionCommand:
fmt.Printf(i18n.Get(i18n.VersionInfo, getVersion()))
case cli.HelpCommand:
cli.WriteUsage(os.Stdout)
case cli.NewCommand:
client := &http.Client{
Transport: &http.Transport{
DialContext: func(_ context.Context, network, addr string) (net.Conn, error) {
return net.DialTimeout(network, addr, 5*time.Second)
},
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
},
}

if len(args.CommandArgs) > 0 {
projectName := utils.CleanProjectName(args.CommandArgs[0])
projectDir, err := filepath.Abs(projectName)
projectName := utils.CleanProjectName(filepath.Base(args.CommandArgs[0]))
projectDir, err := filepath.Abs(args.CommandArgs[0])

if err != nil {
fmt.Fprintf(os.Stderr, i18n.Get(i18n.CannotDetermineProjectDir, projectName))
Expand All @@ -90,17 +91,7 @@ func main() {

result, err := interactive.Run(client, ctx)
if err != nil {
reportLog := cli.HandleAppError("", err)

if hasGlobalLog && reportLog {
fmt.Fprintf(os.Stderr, i18n.Get(i18n.LogHint, config.LogPath(homeDir)))
}

if hasGlobalLog {
log.Fatal(err)
}

os.Exit(1)
cli.ExitWithError(err, "", hasGlobalLog, homeDir)
}

if result.Quit {
Expand All @@ -109,6 +100,33 @@ func main() {
}

command.Generate(client, result.ProjectDir, result.ProjectName, result.Plugins, verboseLogger, hasGlobalLog, ctx)
case cli.OpenAPI:
specPath := args.CommandArgs[0]
projectDir, err := filepath.Abs(".")

if dir, ok := args.CommandOptions[cli.OutDir]; ok {
projectDir, err = filepath.Abs(dir)
}

if err != nil {
fmt.Fprintf(os.Stderr, "Unable to determine project directory %s\n", projectDir)
os.Exit(1)
}

projectName := utils.CleanProjectName(filepath.Base(projectDir))

if _, err := os.Stat(specPath); errors.Is(err, os.ErrNotExist) {
fmt.Printf("OpenAPI spec file %s does not exist\n", specPath)
os.Exit(1)
}

err = command.OpenApi(client, specPath, projectName, projectDir, homeDir, verboseLogger)

if err != nil {
cli.ExitWithError(err, projectDir, hasGlobalLog, homeDir)
}

fmt.Printf("Project %s has been generated in the directory %s\n", projectName, projectDir)
}
}

Expand Down
66 changes: 14 additions & 52 deletions internal/app/cli/command/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ import (
"context"
"fmt"
"github.com/ktorio/ktor-cli/internal/app/cli"
"github.com/ktorio/ktor-cli/internal/app/config"
"github.com/ktorio/ktor-cli/internal/app/generate"
"github.com/ktorio/ktor-cli/internal/app/i18n"
"github.com/ktorio/ktor-cli/internal/app/jdk"
"github.com/ktorio/ktor-cli/internal/app/utils"
"log"
"net/http"
"os"
Expand All @@ -24,64 +22,28 @@ func Generate(client *http.Client, projectDir, projectName string, plugins []str
err = generate.Project(client, verboseLogger, projectDir, projectName, plugins, ctx)

if err != nil {
if _, err := os.Stat(projectDir); err == nil && utils.IsDirEmpty(projectDir) {
_ = os.Remove(projectDir)
}

reportLog := cli.HandleAppError(projectDir, err)

if hasGlobalLog && reportLog {
fmt.Fprintf(os.Stderr, i18n.Get(i18n.LogHint, config.LogPath(homeDir)))
}

if hasGlobalLog {
log.Fatal(err)
}

os.Exit(1)
cli.ExitWithError(err, projectDir, hasGlobalLog, homeDir)
}

fmt.Printf(i18n.Get(i18n.ProjectCreated, projectName, projectDir))

if jh, ok := jdk.JavaHome(); ok {
if v, err := jdk.GetJavaMajorVersion(jh, homeDir); err == nil && v >= jdk.MinJavaVersion {
fmt.Printf(i18n.Get(i18n.JDKDetectedJavaHome, jh))
cli.PrintCommands(projectDir, true, "")
os.Exit(0)
}
}
jdkSrc, jdkPath, err := cli.ObtainJdk(client, verboseLogger, homeDir)

if jdkPath, ok := config.GetValue("jdk"); ok {
if st, err := os.Stat(jdkPath); err == nil && st.IsDir() {
fmt.Printf(i18n.Get(i18n.JdkDetected, jdkPath))
cli.PrintCommands(projectDir, false, jdkPath)
os.Exit(0)
}
}

if jdkPath, ok := jdk.FindLocally(jdk.MinJavaVersion); ok {
config.SetValue("jdk", jdkPath)
_ = config.Commit()
fmt.Printf(i18n.Get(i18n.JdkFoundLocally, jdkPath))
switch jdkSrc {
case jdk.FromJavaHome:
fmt.Printf("JDK is detected in JAVA_HOME=%s\n", jdkPath)
cli.PrintCommands(projectDir, true, "")
case jdk.FromConfig:
fmt.Printf("Detected JDK %s\n", jdkPath)
cli.PrintCommands(projectDir, false, jdkPath)
os.Exit(0)
}

jdkPath, err := cli.DownloadJdk(homeDir, client, verboseLogger, 0)
if err != nil {
reportLog := cli.HandleAppError(projectDir, err)

if hasGlobalLog && reportLog {
fmt.Fprintf(os.Stderr, i18n.Get(i18n.LogHint, config.LogPath(homeDir)))
}

if hasGlobalLog {
log.Fatal(err)
case jdk.Locally:
fmt.Printf("JDK found locally %s\n", jdkPath)
cli.PrintCommands(projectDir, false, jdkPath)
case jdk.Downloaded:
if err != nil {
cli.ExitWithError(err, projectDir, hasGlobalLog, homeDir)
}

os.Exit(1)
}

fmt.Printf(i18n.Get(i18n.JdkDownloaded, jdkPath))
cli.PrintCommands(projectDir, false, jdkPath)
}
108 changes: 108 additions & 0 deletions internal/app/cli/command/openapi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package command

import (
"errors"
"fmt"
"github.com/ktorio/ktor-cli/internal/app"
"github.com/ktorio/ktor-cli/internal/app/cli"
"github.com/ktorio/ktor-cli/internal/app/config"
"github.com/ktorio/ktor-cli/internal/app/jdk"
"github.com/ktorio/ktor-cli/internal/app/network"
"github.com/ktorio/ktor-cli/internal/app/openapi"
"github.com/ktorio/ktor-cli/internal/app/utils"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
)

func OpenApi(client *http.Client, specPath string, projectName, projectDir string, homeDir string, verboseLogger *log.Logger) error {
err := os.MkdirAll(projectDir, 0755)
verboseLogger.Printf("Creating directory %s\n", projectDir)

var pe *os.PathError

if errors.As(err, &pe) && (errors.Is(pe.Err, syscall.EROFS) || errors.Is(pe.Err, syscall.EPERM)) {
return &app.Error{Err: err, Kind: app.ProjectDirError}
}

if _, err := os.Stat(projectDir); errors.Is(err, os.ErrNotExist) {
return &app.Error{Err: err, Kind: app.UnknownError}
} else if !utils.IsDirEmpty(projectDir) {
return &app.Error{Err: &os.PathError{Err: os.ErrExist, Path: projectDir}, Kind: app.ProjectDirError}
}

jarName := filepath.Base(config.OpenApiJarUrl())
jarPath := filepath.Join(config.TempDir(homeDir), jarName)

if _, err := os.Stat(jarPath); errors.Is(err, os.ErrNotExist) {
jarBytes, err := openapi.DownloadJar(client, config.OpenApiJarUrl())

if err != nil {
return err
}

f, err := os.Create(jarPath)
verboseLogger.Printf("Creating OpenAPI JAR file %s\n", jarPath)

if err != nil {
return &app.Error{Err: err, Kind: app.OpenApiDownloadJarError}
}

defer f.Close()

_, err = f.Write(jarBytes)

if err != nil {
return &app.Error{Err: err, Kind: app.OpenApiDownloadJarError}
}
}

src, jdkPath, err := cli.ObtainJdk(client, verboseLogger, homeDir)

if err != nil {
return err
}

if src == jdk.Downloaded {
fmt.Printf("JDK has been downloaded to %s\n", jdkPath)
}

settings, err := network.FetchSettings(client)

if err != nil {
return err
}

javaExec := filepath.Join(jdkPath, "bin", "java")

c := []string{javaExec, "-jar", jarPath, "generate", "-g", "kotlin-server", "-i", specPath,
"--artifact-id", projectName, "--package-name", utils.GetPackage(settings.CompanyWebsite.DefaultVal), "-o", projectDir}

verboseLogger.Printf("Executing command: %s\n", strings.Join(c, " "))

cmd := exec.Command(javaExec, c[1:]...)

stdout, err := cmd.Output()

var ee *exec.ExitError
if errors.As(err, &ee) {
msg := string(ee.Stderr)

if strings.Contains(msg, "Unable to access jarfile") {
return &app.Error{Err: err, Kind: app.OpenApiExecuteJarError}
}

return &app.Error{Err: errors.New(msg), Kind: app.ExternalCommandError}
}

if err != nil {
return &app.Error{Err: err, Kind: app.UnknownError}
}

verboseLogger.Println(string(stdout))
return nil
}
9 changes: 9 additions & 0 deletions internal/app/cli/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const (
CommandNotFoundError
WrongNumberOfArgumentsError
UnrecognizedFlagsError
NoArgumentForFlag
)

type UnrecognizedFlags []string
Expand All @@ -33,3 +34,11 @@ type CommandError struct {
func (e CommandError) Error() string {
return fmt.Sprintf("command '%s' error", e.Command)
}

type FlagError struct {
Flag string
}

func (e FlagError) Error() string {
return fmt.Sprintf("flag '%s' error", e.Flag)
}
30 changes: 30 additions & 0 deletions internal/app/cli/jdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"errors"
"fmt"
"github.com/ktorio/ktor-cli/internal/app"
"github.com/ktorio/ktor-cli/internal/app/config"
"github.com/ktorio/ktor-cli/internal/app/i18n"
"github.com/ktorio/ktor-cli/internal/app/jdk"
"log"
"net/http"
"os"
)

func DownloadJdk(homeDir string, client *http.Client, logger *log.Logger, attempt int) (string, error) {
Expand All @@ -31,3 +33,31 @@ func DownloadJdk(homeDir string, client *http.Client, logger *log.Logger, attemp

return jdkPath, nil
}

func ObtainJdk(client *http.Client, verboseLogger *log.Logger, homeDir string) (jdk.Source, string, error) {
if jh, ok := jdk.JavaHome(); ok {
if v, err := jdk.GetJavaMajorVersion(jh, homeDir); err == nil && v >= jdk.MinJavaVersion {
return jdk.FromJavaHome, jh, nil
}
}

if jdkPath, ok := config.GetValue("jdk"); ok {
if st, err := os.Stat(jdkPath); err == nil && st.IsDir() {
return jdk.FromConfig, jdkPath, nil
}
}

if jdkPath, ok := jdk.FindLocally(jdk.MinJavaVersion); ok {
config.SetValue("jdk", jdkPath)
_ = config.Commit()
return jdk.Locally, jdkPath, nil
}

jdkPath, err := DownloadJdk(homeDir, client, verboseLogger, 0)

if err != nil {
return 0, "", err
}

return jdk.Downloaded, jdkPath, nil
}
Loading

0 comments on commit 80a1f90

Please sign in to comment.