mirror of
https://codeberg.org/scip/esctl.git
synced 2026-08-24 07:54:17 +02:00
add API viewer, mv repl subcommand to api (#41)
This commit is contained in:
503
pkg/es/api.go
Normal file
503
pkg/es/api.go
Normal file
@@ -0,0 +1,503 @@
|
||||
/*
|
||||
Copyright © 2026 Thomas von Dein
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package es
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/scip/esctl/assets"
|
||||
"codeberg.org/scip/esctl/pkg/cfg"
|
||||
"codeberg.org/scip/esctl/pkg/printer"
|
||||
markdown "github.com/MichaelMure/go-term-markdown"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/chzyer/readline"
|
||||
"github.com/go-openapi/spec"
|
||||
)
|
||||
|
||||
const intro = `Input format: verb path [data]"
|
||||
|
||||
Example:
|
||||
|
||||
post /yourindex/_ccr/pause_follow
|
||||
put /yourindex/_settings {"number_of_replicas": 1}
|
||||
|
||||
You can also put multiline JSON after the path like:
|
||||
|
||||
put /yourindex/_settings
|
||||
{
|
||||
"number_of_replicas": 1
|
||||
}
|
||||
|
||||
If you do NOT supply a JSON in the first line, you need to hit ENTER
|
||||
twice to complete.`
|
||||
|
||||
// holds an API operation via go-openapi/spec
|
||||
type Op struct {
|
||||
Verb string
|
||||
Op *spec.Operation
|
||||
}
|
||||
|
||||
type Param struct {
|
||||
Description, Param string
|
||||
}
|
||||
|
||||
// API operation parameter type
|
||||
type Params struct {
|
||||
Path []Param
|
||||
Query []Param
|
||||
}
|
||||
|
||||
func ApiRepl(conf *cfg.Config) error {
|
||||
verbs := []string{"post", "get", "put", "delete"}
|
||||
|
||||
fmt.Println(intro)
|
||||
fmt.Println()
|
||||
|
||||
reader, err := readline.NewEx(&readline.Config{
|
||||
Prompt: "> ",
|
||||
HistoryFile: os.Getenv("HOME") + "/.config/esctl/history",
|
||||
HistoryLimit: 500,
|
||||
InterruptPrompt: "^C",
|
||||
EOFPrompt: "exit",
|
||||
HistorySearchFold: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize readline lib: %s", err)
|
||||
}
|
||||
|
||||
for {
|
||||
text, err := reader.Readline()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
text = strings.TrimSpace(text)
|
||||
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(strings.TrimSpace(text), " ", 3)
|
||||
if len(parts) < 2 {
|
||||
fmt.Println("error: you need to input a verb, uri [and post data]")
|
||||
continue
|
||||
}
|
||||
|
||||
if !slices.Contains(verbs, strings.ToLower(parts[0])) {
|
||||
fmt.Println("error: verb must be one of " + strings.Join(verbs, ","))
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(parts[1], "/") {
|
||||
parts[1] = "/" + parts[1]
|
||||
}
|
||||
|
||||
json := ""
|
||||
if len(parts) == 3 {
|
||||
// put /uri {json}
|
||||
json = parts[2]
|
||||
}
|
||||
|
||||
// put /uri/<Ret> [json]
|
||||
data, err := readJSON(json)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
raw, err := CallAPI(conf, parts[0], parts[1], data)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to call API: %s\n", esErrorString(err))
|
||||
}
|
||||
|
||||
if err := prettyfiJson(conf, raw); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeAuth(username, password string) string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
|
||||
}
|
||||
|
||||
func CallAPI(conf *cfg.Config, verb, path, data string) ([]byte, error) {
|
||||
verb = strings.ToUpper(verb)
|
||||
|
||||
// we're using port-forwards anyway
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
|
||||
client := &http.Client{Transport: tr}
|
||||
|
||||
req, err := http.NewRequest(verb, conf.DefaultCluster.Uri+path, bytes.NewBuffer([]byte(data)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req.Header.Add("accept", "application/json")
|
||||
req.Header.Add("Authorization", "Basic "+encodeAuth(conf.DefaultCluster.User, conf.DefaultCluster.Pass))
|
||||
|
||||
// actually execute the request
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read and print response
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %s", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func prettyfiJson(conf *cfg.Config, raw []byte) error {
|
||||
if conf.HaveJQ {
|
||||
cmd := exec.CommandContext(context.Background(), "jq", "-C")
|
||||
cmd.Stdin = bytes.NewReader(raw)
|
||||
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(out.String())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var pretty bytes.Buffer
|
||||
err := json.Indent(&pretty, raw, "", "\t")
|
||||
if err != nil {
|
||||
return fmt.Errorf("json parse error: %s", err)
|
||||
}
|
||||
|
||||
fmt.Println(pretty.String())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// interactively read arbitrary JSON data from STDIN, which is
|
||||
// virtually a repl inside the primary repl
|
||||
func readJSON(input string) (string, error) {
|
||||
data := ""
|
||||
|
||||
if input != "" {
|
||||
data = input
|
||||
} else {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
if line == "" {
|
||||
break
|
||||
}
|
||||
|
||||
data += line
|
||||
}
|
||||
}
|
||||
|
||||
if data == "" {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// validate
|
||||
check := map[string]any{}
|
||||
err := json.Unmarshal([]byte(data), &check)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error: input data is not proper JSON: %s", err)
|
||||
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func ApiList(conf *cfg.Config, pattern string) error {
|
||||
assets.LoadAssetOpenApi()
|
||||
|
||||
filter := regexp.MustCompile(pattern)
|
||||
|
||||
table := printer.NewTable(conf, 4, 0)
|
||||
table.Addheaders("path", "http verb", "tag", "description")
|
||||
|
||||
for path, item := range assets.OpenAPI.Spec().Paths.Paths {
|
||||
if pattern != "" {
|
||||
if !filter.MatchString(path) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
ops := findOperation(item.PathItemProps)
|
||||
|
||||
for _, op := range ops {
|
||||
tags := findTags(op)
|
||||
|
||||
if conf.Tag != "" {
|
||||
if !slices.Contains(tags, conf.Tag) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
table.AddRow(
|
||||
path,
|
||||
op.Verb,
|
||||
strings.Join(tags, ","),
|
||||
strings.TrimSpace(op.Op.Summary),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
table.Sort()
|
||||
return table.Print()
|
||||
}
|
||||
|
||||
// for completion
|
||||
func ApiPathNames() []string {
|
||||
assets.LoadAssetOpenApi()
|
||||
|
||||
paths := []string{}
|
||||
|
||||
for path := range assets.OpenAPI.Spec().Paths.Paths {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
func ApiShow(conf *cfg.Config, showpath, verb string) error {
|
||||
assets.LoadAssetOpenApi()
|
||||
|
||||
op, err := matchOperation(showpath, verb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
slog.Debug("api operation", "op", op)
|
||||
|
||||
cleanMarkup := regexp.MustCompile(`<[^<>]+>`)
|
||||
|
||||
params := getApiParameters(op, showpath)
|
||||
sample := getApiExample(op)
|
||||
description := markdown.Render(cleanMarkup.ReplaceAllString(op.Op.Description, ""), 80, 6)
|
||||
|
||||
var bold = lipgloss.NewStyle().
|
||||
Bold(true)
|
||||
var paragraph = lipgloss.NewStyle().
|
||||
MarginBottom(1).
|
||||
MarginLeft(4)
|
||||
var boldparagraph = lipgloss.NewStyle().
|
||||
MarginBottom(1).
|
||||
MarginLeft(4).
|
||||
Bold(true)
|
||||
var indentparagraph = lipgloss.NewStyle().
|
||||
MarginBottom(1).
|
||||
MarginLeft(2)
|
||||
|
||||
fmt.Println(bold.Render("ID: " + op.Op.ID))
|
||||
fmt.Println(paragraph.Render(fmt.Sprintf("%s %s", op.Verb, showpath)))
|
||||
|
||||
fmt.Println(bold.Render("Tags"))
|
||||
fmt.Println(paragraph.Render(strings.Join(op.Op.Tags, ", ")))
|
||||
|
||||
fmt.Println(bold.Render("Summary"))
|
||||
fmt.Println(paragraph.Render(strings.TrimSpace(op.Op.Summary)))
|
||||
|
||||
fmt.Println(bold.Render("Description"))
|
||||
fmt.Println(string(description)) // already indented
|
||||
|
||||
if len(params.Path) > 0 {
|
||||
fmt.Println(bold.Render("Path Parameters"))
|
||||
for _, param := range params.Path {
|
||||
fmt.Println(boldparagraph.Render(param.Param))
|
||||
fmt.Println(indentparagraph.Render(param.Description))
|
||||
}
|
||||
}
|
||||
|
||||
if len(params.Query) > 0 {
|
||||
fmt.Println(bold.Render("Query Parameters"))
|
||||
for _, param := range params.Query {
|
||||
fmt.Println(boldparagraph.Render(param.Param))
|
||||
fmt.Println(indentparagraph.Render(param.Description))
|
||||
}
|
||||
}
|
||||
|
||||
if sample != "" {
|
||||
fmt.Println(bold.Render("Example"))
|
||||
fmt.Println(paragraph.Render(sample))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// find all tags associated with op
|
||||
func findTags(op *Op) []string {
|
||||
return op.Op.Tags
|
||||
}
|
||||
|
||||
// Get API parameters
|
||||
func getApiParameters(op *Op, path string) Params {
|
||||
params := Params{}
|
||||
pathParams := []string{}
|
||||
pathParamsReg := regexp.MustCompile(`{([a-z_]+)}`)
|
||||
|
||||
// find params in url path
|
||||
for _, match := range pathParamsReg.FindAllStringSubmatch(path, -1) {
|
||||
if len(match) == 2 {
|
||||
pathParams = append(pathParams, match[1])
|
||||
}
|
||||
}
|
||||
|
||||
empty := spec.ParamProps{}
|
||||
for _, param := range op.Op.Parameters {
|
||||
if param.ParamProps == empty {
|
||||
for _, token := range param.Refable.Ref.Ref.GetPointer().DecodedTokens() {
|
||||
parts := strings.Split(token, "-")
|
||||
if len(parts) == 2 {
|
||||
param := parts[1]
|
||||
if slices.Contains(pathParams, param) {
|
||||
params.Path = append(params.Path, Param{Param: param})
|
||||
} else {
|
||||
params.Query = append(params.Query, Param{Param: param})
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
par := Param{
|
||||
Description: string(markdown.Render(param.Description, 80, 6)),
|
||||
Param: param.Name,
|
||||
}
|
||||
|
||||
if param.In == "query" {
|
||||
params.Query = append(params.Query, par)
|
||||
} else {
|
||||
params.Path = append(params.Path, par)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
// Get console example
|
||||
func getApiExample(op *Op) string {
|
||||
sample := ""
|
||||
|
||||
samples, sampleexist := op.Op.Extensions["x-codeSamples"]
|
||||
if sampleexist {
|
||||
for _, itemany := range samples.([]any) {
|
||||
item := itemany.(map[string]any)
|
||||
|
||||
lang, haslang := item["lang"]
|
||||
if haslang {
|
||||
if lang == "Console" {
|
||||
source, hassource := item["source"]
|
||||
if hassource {
|
||||
sample = strings.TrimSpace(source.(string))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return sample
|
||||
}
|
||||
|
||||
// Find an API operation which matches given path and verb.
|
||||
// If no verb is given and only 1 op exists, return this one,
|
||||
// otherwise showpath+verb have to match precisely.
|
||||
func matchOperation(showpath, verb string) (*Op, error) {
|
||||
ops := []*Op{}
|
||||
op := &Op{}
|
||||
|
||||
if verb == "" {
|
||||
verb = "get"
|
||||
}
|
||||
|
||||
for path, item := range assets.OpenAPI.Spec().Paths.Paths {
|
||||
if path == showpath {
|
||||
ops = findOperation(item.PathItemProps)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(ops) == 0 {
|
||||
return nil, errors.New("no matching API call found")
|
||||
}
|
||||
|
||||
for _, item := range ops {
|
||||
if strings.ToLower(item.Verb) == verb {
|
||||
op = item
|
||||
}
|
||||
}
|
||||
|
||||
if op == nil {
|
||||
return nil, errors.New("no matching API call found for given path+verb")
|
||||
}
|
||||
|
||||
return op, nil
|
||||
}
|
||||
|
||||
func findOperation(item spec.PathItemProps) []*Op {
|
||||
ops := []*Op{}
|
||||
|
||||
if item.Post != nil {
|
||||
ops = append(ops, &Op{"POST", item.Post})
|
||||
}
|
||||
|
||||
if item.Get != nil {
|
||||
ops = append(ops, &Op{"GET", item.Get})
|
||||
}
|
||||
|
||||
if item.Delete != nil {
|
||||
ops = append(ops, &Op{"DELETE", item.Delete})
|
||||
}
|
||||
|
||||
if item.Put != nil {
|
||||
ops = append(ops, &Op{"PUT", item.Put})
|
||||
}
|
||||
|
||||
if item.Patch != nil {
|
||||
ops = append(ops, &Op{"PATCH", item.Patch})
|
||||
}
|
||||
|
||||
return ops
|
||||
}
|
||||
226
pkg/es/repl.go
226
pkg/es/repl.go
@@ -1,226 +0,0 @@
|
||||
/*
|
||||
Copyright © 2026 Thomas von Dein
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package es
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/scip/esctl/pkg/cfg"
|
||||
"github.com/chzyer/readline"
|
||||
)
|
||||
|
||||
const intro = `Input format: verb path [data]"
|
||||
|
||||
Example:
|
||||
|
||||
post /yourindex/_ccr/pause_follow
|
||||
put /yourindex/_settings {"number_of_replicas": 1}
|
||||
|
||||
You can also put multiline JSON after the path like:
|
||||
|
||||
put /yourindex/_settings
|
||||
{
|
||||
"number_of_replicas": 1
|
||||
}
|
||||
|
||||
If you do NOT supply a JSON in the first line, you need to hit ENTER
|
||||
twice to complete.`
|
||||
|
||||
func Repl(conf *cfg.Config) error {
|
||||
verbs := []string{"post", "get", "put", "delete"}
|
||||
|
||||
fmt.Println(intro)
|
||||
fmt.Println()
|
||||
|
||||
reader, err := readline.NewEx(&readline.Config{
|
||||
Prompt: "> ",
|
||||
HistoryFile: os.Getenv("HOME") + "/.config/esctl/history",
|
||||
HistoryLimit: 500,
|
||||
InterruptPrompt: "^C",
|
||||
EOFPrompt: "exit",
|
||||
HistorySearchFold: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize readline lib: %s", err)
|
||||
}
|
||||
|
||||
for {
|
||||
text, err := reader.Readline()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
text = strings.TrimSpace(text)
|
||||
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(strings.TrimSpace(text), " ", 3)
|
||||
if len(parts) < 2 {
|
||||
fmt.Println("error: you need to input a verb, uri [and post data]")
|
||||
continue
|
||||
}
|
||||
|
||||
if !slices.Contains(verbs, strings.ToLower(parts[0])) {
|
||||
fmt.Println("error: verb must be one of " + strings.Join(verbs, ","))
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(parts[1], "/") {
|
||||
parts[1] = "/" + parts[1]
|
||||
}
|
||||
|
||||
json := ""
|
||||
if len(parts) == 3 {
|
||||
// put /uri {json}
|
||||
json = parts[2]
|
||||
}
|
||||
|
||||
// put /uri/<Ret> [json]
|
||||
data, err := readJSON(json)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
raw, err := CallAPI(conf, parts[0], parts[1], data)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to call API: %s\n", esErrorString(err))
|
||||
}
|
||||
|
||||
if err := prettyfiJson(conf, raw); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeAuth(username, password string) string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
|
||||
}
|
||||
|
||||
func CallAPI(conf *cfg.Config, verb, path, data string) ([]byte, error) {
|
||||
verb = strings.ToUpper(verb)
|
||||
|
||||
// we're using port-forwards anyway
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
|
||||
client := &http.Client{Transport: tr}
|
||||
|
||||
req, err := http.NewRequest(verb, conf.DefaultCluster.Uri+path, bytes.NewBuffer([]byte(data)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req.Header.Add("accept", "application/json")
|
||||
req.Header.Add("Authorization", "Basic "+encodeAuth(conf.DefaultCluster.User, conf.DefaultCluster.Pass))
|
||||
|
||||
// actually execute the request
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read and print response
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %s", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func prettyfiJson(conf *cfg.Config, raw []byte) error {
|
||||
if conf.HaveJQ {
|
||||
cmd := exec.CommandContext(context.Background(), "jq", "-C")
|
||||
cmd.Stdin = bytes.NewReader(raw)
|
||||
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(out.String())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var pretty bytes.Buffer
|
||||
err := json.Indent(&pretty, raw, "", "\t")
|
||||
if err != nil {
|
||||
return fmt.Errorf("json parse error: %s", err)
|
||||
}
|
||||
|
||||
fmt.Println(pretty.String())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// interactively read arbitrary JSON data from STDIN, which is
|
||||
// virtually a repl inside the primary repl
|
||||
func readJSON(input string) (string, error) {
|
||||
data := ""
|
||||
|
||||
if input != "" {
|
||||
data = input
|
||||
} else {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
if line == "" {
|
||||
break
|
||||
}
|
||||
|
||||
data += line
|
||||
}
|
||||
}
|
||||
|
||||
if data == "" {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// validate
|
||||
check := map[string]any{}
|
||||
err := json.Unmarshal([]byte(data), &check)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error: input data is not proper JSON: %s", err)
|
||||
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
Reference in New Issue
Block a user