Skip to content

Commit

Permalink
KTOR-7940 Support adding dependencies to an existing Ktor project (#44)
Browse files Browse the repository at this point in the history
  • Loading branch information
Stexxe authored Feb 5, 2025
1 parent e63706e commit 11c0261
Show file tree
Hide file tree
Showing 198 changed files with 59,088 additions and 51 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,24 @@ You can specify a different output directory with the `-o` or `--output` flag:
ktor openapi -o path/to/project petstore.yaml
```

## Add a Ktor dependency to an existing project

To add a Ktor dependency to a Gradle project in the current working directory, use the `add` command:
```shell
ktor add server-core
```
Use the `-p` or `--project` option to specify a path to the project directory:
```shell
ktor add -p /path/to/project server-core
```

You can add multiple modules with a single command:
```shell
ktor add -p /path/to/project server-core client-core json
```

Currently, Ktor dependencies can only be added to **non-multiplatform** Gradle projects using Kotlin DSL.

## Get the version

To get the version of the tool, use the `--version` flag or the `version` command:
Expand Down
219 changes: 217 additions & 2 deletions cmd/ktor/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"bufio"
"context"
_ "embed"
"errors"
Expand All @@ -10,6 +11,12 @@ import (
"github.com/ktorio/ktor-cli/internal/app/config"
"github.com/ktorio/ktor-cli/internal/app/i18n"
"github.com/ktorio/ktor-cli/internal/app/interactive"
"github.com/ktorio/ktor-cli/internal/app/ktor"
"github.com/ktorio/ktor-cli/internal/app/lang"
"github.com/ktorio/ktor-cli/internal/app/lang/gradle"
"github.com/ktorio/ktor-cli/internal/app/lang/toml"
"github.com/ktorio/ktor-cli/internal/app/network"
"github.com/ktorio/ktor-cli/internal/app/project"
"github.com/ktorio/ktor-cli/internal/app/utils"
"io"
"log"
Expand All @@ -18,13 +25,20 @@ import (
"os"
"path/filepath"
"runtime/debug"
"slices"
"strings"
"time"
)

var Version string

func main() {
defer func() {
if e := recover(); e != nil {
fmt.Print(i18n.Get(i18n.UnrecoverableErrorBlock, e, string(debug.Stack())))
}
}()

args, err := cli.ProcessArgs(cli.ParseArgs(os.Args))

if err != nil {
Expand Down Expand Up @@ -71,10 +85,211 @@ func main() {
}

switch args.Command {
case cli.AddCommand:
modules := args.CommandArgs

projectDir := "."
if dir, ok := args.CommandOptions[cli.ProjectDir]; ok {
projectDir = dir
}

projectDir, err = filepath.Abs(projectDir)

if err != nil {
fmt.Fprintf(os.Stderr, i18n.Get(i18n.CannotDetermineProjectDir, projectDir))
os.Exit(1)
}

verboseLogger.Print(i18n.Get(i18n.ProjectAddMessage, projectDir))

tomlPath, tomlFound := toml.FindVersionsPath(projectDir)
var tomlDoc *toml.Document
var tomlSuccessParsed bool

buildPath := filepath.Join(projectDir, "build.gradle.kts")
buildFound := utils.Exists(buildPath)
var buildRoot *gradle.BuildRoot

if buildFound {
var syntaxErrors []lang.SyntaxError
buildRoot, err, syntaxErrors = gradle.ParseBuildFile(buildPath)

var sErrors []lang.SyntaxError
if len(sErrors) > 0 {
if len(syntaxErrors) < 5 {
sErrors = syntaxErrors
} else {
sErrors = syntaxErrors[:5]
}

log.Println(lang.StringifySyntaxErrors(sErrors))
}

if err != nil {
cli.ExitWithError(err, hasGlobalLog, homeDir)
} else {
if tomlFound {
tomlDoc, err, syntaxErrors = toml.ParseCatalogToml(tomlPath)

if len(syntaxErrors) > 0 {
if len(syntaxErrors) < 5 {
sErrors = syntaxErrors
} else {
sErrors = syntaxErrors[:5]
}

log.Println(lang.StringifySyntaxErrors(sErrors))
}

if err != nil {
log.Println(err)
} else {
tomlSuccessParsed = true
}
}

if project.IsKmp(buildRoot, tomlDoc, tomlSuccessParsed) {
fmt.Fprintln(os.Stderr, i18n.Get(i18n.AddKtorModulesToKmpError))
os.Exit(1)
}
}
} else {
if utils.Exists(filepath.Join(projectDir, "pom.xml")) {
fmt.Fprintln(os.Stderr, i18n.Get(i18n.AddKtorModulesToMavenError))
} else if utils.Exists(filepath.Join(projectDir, "build.gradle")) {
fmt.Fprintln(os.Stderr, i18n.Get(i18n.AddKtorModulesToGradleGroovyError))
} else {
fmt.Fprintf(os.Stderr, i18n.Get(i18n.UnableToFindBuildGradleKts, projectDir))
}
os.Exit(1)
}

var ktorVersion string
if v, ok := project.SearchKtorVersion(projectDir, buildRoot, tomlDoc, tomlSuccessParsed); ok {
ktorVersion = v
verboseLogger.Printf(i18n.Get(i18n.DetectedKtorVersion, ktorVersion))
} else {
settings, err := network.FetchSettings(client)

if err != nil {
cli.ExitWithError(err, hasGlobalLog, homeDir)
} else {
ktorVersion = settings.KtorVersion.DefaultId
verboseLogger.Printf(i18n.Get(i18n.UseLatestKtorVersion, ktorVersion))
}
}

artifacts, err := network.SearchArtifacts(client, ktorVersion, modules)

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

fmt.Print(i18n.Get(i18n.ChangesWarningBlock, filepath.Base(projectDir)))

for i, mod := range modules {
if i > 0 {
buildRoot, err, _ = gradle.ParseBuildFile(buildPath)

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

tomlDoc, err, _ = toml.ParseCatalogToml(tomlPath)
tomlSuccessParsed = err == nil
}

mc, modResult, candidates := ktor.FindModule(artifacts[mod])

if mc.Version == "" {
mc.Version = ktorVersion
}

switch modResult {
case ktor.ModuleNotFound:
fmt.Fprintf(os.Stderr, i18n.Get(i18n.UnableToRecognizeKtorModule, mod))
os.Exit(1)
case ktor.ModuleAmbiguity:
var names []string
for _, c := range candidates {
if !slices.Contains(names, c.Artifact) {
names = append(names, c.Artifact)
}
}
fmt.Fprintf(os.Stderr, i18n.Get(i18n.KtorModuleAmbiguity, strings.Join(names, ", ")))
os.Exit(1)
case ktor.SimilarModulesFound:
fmt.Fprintf(os.Stderr, i18n.Get(i18n.UnableToRecognizeKtorModule, mod))

if len(candidates) > 0 {
fmt.Fprintf(os.Stderr, i18n.Get(i18n.SimilarModuleQuestion, candidates[0].Artifact))
}
os.Exit(1)
case ktor.ModuleFound:
verboseLogger.Printf(i18n.Get(i18n.ChosenKtorModule, mc.String()))
depPlugins := ktor.DependentPlugins(mc)
var serPlugin *ktor.GradlePlugin
if len(depPlugins) > 0 {
serPlugin = &depPlugins[0]
}

files, err := project.AddKtorModule(mc, buildRoot, tomlDoc, tomlSuccessParsed, serPlugin, buildPath, tomlPath, projectDir)

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

if len(files) > 0 {
fmt.Println()
for _, f := range files {
fmt.Println(utils.GetDiff(f.Path, f.Content))
}

fmt.Print(i18n.Get(i18n.ApplyChangesQuestion))
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
answer := scanner.Text()

if answer == "y" || answer == "Y" || answer == "yes" || answer == "Yes" {
err = project.ApplyChanges(files)

if err == nil {
fmt.Println(i18n.Get(i18n.ChangesApplied))
} else {
cli.ExitWithError(err, hasGlobalLog, homeDir)
}
}
} else {
fmt.Println()
fmt.Println(i18n.Get(i18n.NoChanges, mc.String()))
}
}
}
case cli.VersionCommand:
fmt.Printf(i18n.Get(i18n.VersionInfo, getVersion()))
case cli.HelpCommand:
cli.WriteUsage(os.Stdout)
case cli.CompletionCommand:
settings, err := network.FetchSettings(client)

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

shell := args.CommandArgs[0]
modules, err := network.ListArtifacts(client, settings.KtorVersion.DefaultId)

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

s, err := command.Complete(modules, shell)

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

fmt.Print(s)
case cli.NewCommand:
if len(args.CommandArgs) > 0 {
projectName := utils.CleanProjectName(filepath.Base(args.CommandArgs[0]))
Expand All @@ -91,7 +306,7 @@ func main() {

result, err := interactive.Run(client, ctx)
if err != nil {
cli.ExitWithError(err, "", hasGlobalLog, homeDir)
cli.ExitWithError(err, hasGlobalLog, homeDir)
}

if result.Quit {
Expand Down Expand Up @@ -123,7 +338,7 @@ func main() {
err = command.OpenApi(client, specPath, projectName, projectDir, homeDir, verboseLogger)

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

fmt.Printf(i18n.Get(i18n.ProjectCreated, projectName, projectDir))
Expand Down
9 changes: 7 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
module github.com/ktorio/ktor-cli

go 1.21.1
go 1.22.0

toolchain go1.23.3

require (
github.com/antlr4-go/antlr/v4 v4.13.1
github.com/gdamore/tcell/v2 v2.7.4
github.com/hexops/gotextdiff v1.0.3
golang.org/x/term v0.24.0
)

Expand All @@ -12,6 +16,7 @@ require (
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/text v0.18.0 // indirect
)
10 changes: 8 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
Expand All @@ -15,6 +19,8 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
Expand All @@ -31,8 +37,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
Expand Down
Loading

0 comments on commit 11c0261

Please sign in to comment.