From 6a60db07735e5763b791c494b98fbd89e5bb6139 Mon Sep 17 00:00:00 2001 From: Marvin Wendt Date: Mon, 5 Feb 2024 15:53:56 +0100 Subject: [PATCH] feat: added `serve` command that exposes an API --- .bruno/Parse Template.bru | 17 +++++ .bruno/bruno.json | 5 ++ cmd/parse.go | 3 - cmd/root.go | 16 ++++- cmd/serve.go | 84 ++++++++++++++++++++++ go.mod | 10 ++- go.sum | 19 ++++- pkg/parser/parser.go | 145 ++++++++++++++++++++------------------ 8 files changed, 222 insertions(+), 77 deletions(-) create mode 100644 .bruno/Parse Template.bru create mode 100644 .bruno/bruno.json create mode 100644 cmd/serve.go diff --git a/.bruno/Parse Template.bru b/.bruno/Parse Template.bru new file mode 100644 index 0000000..6cfb75d --- /dev/null +++ b/.bruno/Parse Template.bru @@ -0,0 +1,17 @@ +meta { + name: Parse Template + type: http + seq: 1 +} + +post { + url: http://127.0.0.1:8080/api/v1/parse + body: json + auth: none +} + +body:json { + { + "template": "variables:\n - name: Text\n type: text\n regex: ^[a-z]+$\n value: abc\ntemplate: |-\n \{{ .Text }}" + } +} diff --git a/.bruno/bruno.json b/.bruno/bruno.json new file mode 100644 index 0000000..c34fccb --- /dev/null +++ b/.bruno/bruno.json @@ -0,0 +1,5 @@ +{ + "version": "1", + "name": "GTTP API", + "type": "collection" +} \ No newline at end of file diff --git a/cmd/parse.go b/cmd/parse.go index 68814bf..9d98a0d 100644 --- a/cmd/parse.go +++ b/cmd/parse.go @@ -12,10 +12,7 @@ import ( func init() { rootCmd.AddCommand(parseCmd) - // URL flag parseCmd.Flags().StringP("url", "u", "", "Fetch template from URL") - - // File flag parseCmd.Flags().StringP("file", "f", "", "Fetch template from file") } diff --git a/cmd/root.go b/cmd/root.go index 77cd0c7..ac7fe61 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,6 +5,7 @@ import ( "github.com/gttp-cli/gttp/pkg/model" "github.com/gttp-cli/gttp/pkg/parser" "github.com/gttp-cli/gttp/pkg/utils" + "github.com/pterm/pterm" "github.com/spf13/cobra" clip "golang.design/x/clipboard" "os" @@ -16,6 +17,7 @@ func init() { rootCmd.Flags().StringP("output", "o", "", "Output file") rootCmd.Flags().BoolP("clipboard", "c", false, "Copy output to clipboard") rootCmd.Flags().BoolP("silent", "s", false, "Silent mode") + rootCmd.Flags().BoolP("debug", "d", false, "Print debug information") } var rootCmd = &cobra.Command{ @@ -27,6 +29,7 @@ var rootCmd = &cobra.Command{ output, _ := cmd.Flags().GetString("output") silent, _ := cmd.Flags().GetBool("silent") clipboard, _ := cmd.Flags().GetBool("clipboard") + debug, _ := cmd.Flags().GetBool("debug") // Do not allow both URL and file flags to be set if url != "" && file != "" { @@ -56,7 +59,12 @@ var rootCmd = &cobra.Command{ return err } - result, err := parser.ParseTemplate(tmpl) + tmpl, err = parser.ParseTemplate(tmpl) + if err != nil { + return err + } + + result, err := parser.RenderTemplate(tmpl) if err != nil { return err } @@ -79,6 +87,12 @@ var rootCmd = &cobra.Command{ fmt.Println(result) } + if debug { + pterm.DefaultSection.Println("Debug Information") + pterm.DefaultSection.WithLevel(2).Println("Template") + pterm.Printfln("%#v", tmpl) + } + return nil }, } diff --git a/cmd/serve.go b/cmd/serve.go new file mode 100644 index 0000000..d2a9b7c --- /dev/null +++ b/cmd/serve.go @@ -0,0 +1,84 @@ +package cmd + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" + "github.com/gttp-cli/gttp/pkg/model" + "github.com/gttp-cli/gttp/pkg/parser" + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(serveCmd) + + // Add address flag + serveCmd.Flags().StringP("address", "a", "localhost:8080", "Address to listen on") +} + +var serveCmd = &cobra.Command{ + Use: "serve", + Short: "Start API server", + RunE: func(cmd *cobra.Command, args []string) error { + addr, _ := cmd.Flags().GetString("address") + app := fiber.New() + + app.Use(logger.New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.JSON(map[string]string{ + "status": "ok", + "docs": "https://docs.gttp.dev", + }) + }) + + api := app.Group("/api") + v1 := api.Group("/v1") + + // /parse accepts YAML and returns the parsed template as JSON + v1.Post("/parse", func(c *fiber.Ctx) error { + // Get template from JSON "template" key + body := struct { + Template string `json:"template"` + }{} + + if err := c.BodyParser(&body); err != nil { + return c.Status(400).JSON(map[string]string{ + "error": err.Error(), + }) + } + + tmpl, err := model.FromYAML(body.Template) + if err != nil { + return c.Status(400).JSON(map[string]string{ + "error": err.Error(), + }) + } + + // Validate template + errs := tmpl.Validate() + if errs != nil { + var errors []string + for _, err := range errs { + errors = append(errors, err.Error()) + } + return c.Status(400).JSON(map[string]interface{}{ + "errors": errors, + }) + } + + rendered, err := parser.RenderTemplate(tmpl) + if err != nil { + return c.Status(500).JSON(map[string]string{ + "error": err.Error(), + }) + } + + return c.JSON(map[string]string{ + "template": body.Template, + "rendered": rendered, + }) + }) + + return app.Listen(addr) + }, +} diff --git a/go.mod b/go.mod index f899e22..234c733 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/Masterminds/sprig/v3 v3.2.3 github.com/expr-lang/expr v1.16.0 github.com/goccy/go-yaml v1.11.3 + github.com/gofiber/fiber/v2 v2.52.0 github.com/invopop/jsonschema v0.12.0 github.com/pterm/pterm v0.12.76 github.com/spf13/cobra v1.8.0 @@ -18,15 +19,17 @@ require ( atomicgo.dev/schedule v0.1.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.2.0 // indirect + github.com/andybalholm/brotli v1.0.5 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/containerd/console v1.0.3 // indirect github.com/fatih/color v1.16.0 // indirect - github.com/google/uuid v1.1.1 // indirect + github.com/google/uuid v1.5.0 // indirect github.com/gookit/color v1.5.4 // indirect github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.11 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/klauspost/compress v1.17.0 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/mailru/easyjson v0.7.7 // indirect @@ -40,9 +43,12 @@ require ( github.com/shopspring/decimal v1.2.0 // indirect github.com/spf13/cast v1.3.1 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/crypto v0.7.0 // indirect + golang.org/x/crypto v0.14.0 // indirect golang.org/x/exp/shiny v0.0.0-20240119083558-1b970713d09a // indirect golang.org/x/image v0.14.0 // indirect golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect diff --git a/go.sum b/go.sum index 3ca7ad0..8480061 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,8 @@ github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7Y github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= @@ -44,10 +46,13 @@ github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7a github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/goccy/go-yaml v1.11.3 h1:B3W9IdWbvrUu2OYQGwvU1nZtvMQJPBKgBUuweJjLj6I= github.com/goccy/go-yaml v1.11.3/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= +github.com/gofiber/fiber/v2 v2.52.0 h1:S+qXi7y+/Pgvqq4DrSmREGiFwtB7Bu6+QFLuIHYw/UE= +github.com/gofiber/fiber/v2 v2.52.0/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= @@ -61,6 +66,8 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= +github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= @@ -123,6 +130,12 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= @@ -134,8 +147,8 @@ golang.design/x/clipboard v0.7.0/go.mod h1:PQIvqYO9GP29yINEfsEn5zSQKAz3UgXmZKzDA 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/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/exp/shiny v0.0.0-20240119083558-1b970713d09a h1:NZ9mAQhIcCceDZKqQX3JJVIz7nn3QLDuC+nXedsViBM= diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go index 878d17f..860bc8e 100644 --- a/pkg/parser/parser.go +++ b/pkg/parser/parser.go @@ -11,10 +11,8 @@ import ( "text/template" ) -// ParseTemplate parses the template and returns the filled template string. -func ParseTemplate(template model.Template) (string, error) { - variableValues := make(map[string]interface{}) - +// ParseTemplate parses the template and updates its variables with filled values. +func ParseTemplate(template model.Template) (model.Template, error) { // Validate the template validationErrors := template.Validate() if validationErrors != nil { @@ -22,83 +20,89 @@ func ParseTemplate(template model.Template) (string, error) { for _, err := range validationErrors { errors = append(errors, fmt.Sprintf("- %s", err)) } - return "", fmt.Errorf("template validation failed:\n\n%s", strings.Join(errors, "\n")) + return template, fmt.Errorf("template validation failed:\n\n%s", strings.Join(errors, "\n")) } - // Parse and fill in the variables - for _, variable := range template.Variables { - var value any + for i, variable := range template.Variables { + if variable.Value != nil { + continue // Skip variables that already have a value set. + } + var err error + template.Variables[i].Value, err = processVariable(variable, template) + if err != nil { + return template, err + } + } - // Check variable condition; skip if condition is not met - if variable.Condition != "" { - exp, err := expr.Compile(variable.Condition) - if err != nil { - return "", err - } + return template, nil +} - // Evaluate the expression - result, err := expr.Run(exp, variableValues) - if err != nil { - return "", err - } +func processVariable(variable model.Variable, template model.Template) (any, error) { + if variable.Condition != "" && !evaluateCondition(variable.Condition, template) { + return nil, nil // Condition not met, skip variable. + } - // Check if the condition is met - if result != true { - continue - } - } + if strings.HasSuffix(variable.Type, "[]") { + variable.IsArray = true + variable.Type = strings.TrimSuffix(variable.Type, "[]") + } - // Check if variable type indicates an array - if strings.HasSuffix(variable.Type, "[]") { - variable.IsArray = true - variable.Type = strings.TrimSuffix(variable.Type, "[]") - } + if variable.IsArray { + return processArrayVariable(variable, template) + } - // Check if the variable is an array - if variable.IsArray { - var values []interface{} - // Continue to ask for input until the user decides not to add more - for { - if _, ok := template.Structures[variable.Type]; ok { - // Custom type within an array - val, err := ParseCustomType(variable, template.Structures[variable.Type]) - if err != nil { - return "", err - } - values = append(values, val) - } else { - // Base type within an array - val, err := AskForInput(variable, "") - if err != nil { - return "", err - } - values = append(values, val) - } + return processSingleVariable(variable, template) +} - if !AskToContinue() { - break - } - } - value = values - } else { - if _, ok := template.Structures[variable.Type]; ok { - // Single custom type - value, err = ParseCustomType(variable, template.Structures[variable.Type]) - } else { - // Single base type - value, err = AskForInput(variable, "") - } - if err != nil { - return "", err - } +func evaluateCondition(condition string, template model.Template) bool { + exp, err := expr.Compile(condition) + if err != nil { + return false + } + + variableValues := extractVariableValues(template) + result, err := expr.Run(exp, variableValues) + if err != nil { + return false + } + + return result == true +} + +func processArrayVariable(variable model.Variable, template model.Template) ([]interface{}, error) { + var values []interface{} + for { + val, err := askForVariableValue(variable, template) + if err != nil { + return nil, err } - variableValues[variable.Name] = value + values = append(values, val) + if !AskToContinue() { + break + } } + return values, nil +} - // Use ParseGoTextTemplate to parse the Go text template - return ParseGoTextTemplate(template.Template, variableValues) +func processSingleVariable(variable model.Variable, template model.Template) (interface{}, error) { + return askForVariableValue(variable, template) +} + +func askForVariableValue(variable model.Variable, template model.Template) (any, error) { + if structVars, ok := template.Structures[variable.Type]; ok { + return ParseCustomType(variable, structVars) + } + return AskForInput(variable, "") +} + +func extractVariableValues(template model.Template) map[string]interface{} { + values := make(map[string]interface{}) + for _, variable := range template.Variables { + values[variable.Name] = variable.Value + } + return values } func AskToContinue() bool { @@ -224,3 +228,8 @@ func ParseGoTextTemplate(templateContent string, variables map[string]any) (stri return parsed.String(), nil } + +func RenderTemplate(template model.Template) (string, error) { + variableValues := extractVariableValues(template) + return ParseGoTextTemplate(template.Template, variableValues) +}