diff --git a/README.md b/README.md index f8c2b1d..b754e64 100644 --- a/README.md +++ b/README.md @@ -162,12 +162,33 @@ The metadata file is automatically read by Gemini to generate appropriate argume "medium", "large" ] - } - + }, + { + "name": "-f, --file ", + "description": "file to read if you pass - the stdin is read instead", + "file": true, + "rawdata": true + }, ] } ``` -The parameters passed from CLI will overwrite the same parameter if contained in `filename.metadata.json` +#### Field Descriptions: + +* **description**: A text description of the command, explaining its purpose or behavior. +* **arguments**: + * ***name***: The name of the argument. Use angle brackets (``) for required arguments and square brackets (`[arg]`) for optional ones. + * ***description(optional)***: A brief explanation of what the argument represents or its purpose. +* **options**: + * ***name***: The flag name(s), including shorthand (`-n`) and long-form (`--name`) options. + * ***hidden (optional)***: If true, the flag is hidden from the help menu. + * ***description (optional)***: A brief explanation of the flag’s purpose. + * ***default (optional)***: The default value for the flag if not explicitly provided. + * ***env (optional)***: A list of environment variable names that can be used as fallback values for the flag. + * ***choices (optional)***: An array of allowed values for the flag, ensuring users provide a valid input. + * ***file (optional)***: If set to `true`, the flag requires a JSON file path. The file's contents will be added to the slangroom input data. + * ***rawdata (optional)***: If set to true alongside `file: true`, the contents of the file will be added as raw data, with the flag name serving as the key. + +All values provided through arguments and flags are added to the slangroom input data as key-value pairs in the format `"flag_name": "value"`. If a parameter is present in both the CLI input and the corresponding `filename.data.json` file, the CLI input will take precedence, overwriting the value in the JSON file. ### Examples diff --git a/cmd/cli.go b/cmd/cli.go index 3453151..ce235a3 100644 --- a/cmd/cli.go +++ b/cmd/cli.go @@ -122,9 +122,11 @@ func addEmbeddedFileCommands() { Short: fmt.Sprintf("Execute the embedded contract %s", strings.TrimSuffix(file.FileName, filepath.Ext(file.FileName))), } var isMetadata bool - argContents := make(map[string]string) + argContents := make(map[string]interface{}) flagContents := make(map[string]utils.FlagData) + input := slangroom.SlangroomInput{Contract: file.Content} + metadataPath := filepath.Join(file.Dir, strings.TrimSuffix(file.FileName, filepath.Ext(file.FileName))+".metadata.json") metadata, err := utils.LoadMetadata(&contracts, metadataPath) if err != nil && err.Error() != "metadata file not found" { @@ -140,13 +142,13 @@ func addEmbeddedFileCommands() { os.Exit(1) } fileCmd.PreRunE = func(cmd *cobra.Command, _ []string) error { - return utils.ValidateFlags(cmd, flagContents, argContents) + return utils.ValidateFlags(cmd, flagContents, argContents, &input) } } // Set the command's run function fileCmd.Run = func(_ *cobra.Command, args []string) { - runFileCommand(file, args, metadata, argContents, isMetadata, relativePath) + runFileCommand(file, args, metadata, argContents, isMetadata, relativePath, &input) } // Add the file command to its directory's command @@ -217,10 +219,9 @@ var runCmd = &cobra.Command{ }, } -func runFileCommand(file fouter.SlangFile, args []string, metadata *utils.CommandMetadata, argContents map[string]string, isMetadata bool, relativePath string) { - input := slangroom.SlangroomInput{Contract: file.Content} +func runFileCommand(file fouter.SlangFile, args []string, metadata *utils.CommandMetadata, argContents map[string]interface{}, isMetadata bool, relativePath string, input *slangroom.SlangroomInput) { filename := strings.TrimSuffix(file.FileName, extension) - err := utils.LoadAdditionalData(file.Dir, filename, &input) + err := utils.LoadAdditionalData(file.Dir, filename, input) if err != nil { log.Printf("Failed to load data from JSON file: %v\n", err) os.Exit(1) @@ -248,7 +249,7 @@ func runFileCommand(file fouter.SlangFile, args []string, metadata *utils.Comman } // Start HTTP server if daemon flag is set if daemon { - if err := httpserver.StartHTTPServer("contracts", relativePath, &input); err != nil { + if err := httpserver.StartHTTPServer("contracts", relativePath, input); err != nil { log.Printf("Failed to start HTTP server: %v\n", err) os.Exit(1) } @@ -256,7 +257,7 @@ func runFileCommand(file fouter.SlangFile, args []string, metadata *utils.Comman } // Execute the slangroom file - res, err := slangroom.Exec(input) + res, err := slangroom.Exec(*input) if err != nil { log.Println("Error:", err) log.Println(res.Logs) diff --git a/cmd/utils/utils.go b/cmd/utils/utils.go index 808a50a..64da2a7 100644 --- a/cmd/utils/utils.go +++ b/cmd/utils/utils.go @@ -29,6 +29,7 @@ type CommandMetadata struct { Env []string `json:"env,omitempty"` Hidden bool `json:"hidden,omitempty"` File bool `json:"file,omitempty"` + RawData bool `json:"rawdata,omitempty"` } `json:"options"` } @@ -36,7 +37,7 @@ type CommandMetadata struct { type FlagData struct { Choices []string Env []string - File bool + File [2]bool } // LoadAdditionalData loads and validates JSON data for additional fields in SlangroomInput. @@ -172,8 +173,8 @@ func MergeJSON(json1, json2 string) (string, error) { } // ConfigureArgumentsAndFlags configures the command's arguments and flags based on provided metadata, -func ConfigureArgumentsAndFlags(fileCmd *cobra.Command, metadata *CommandMetadata) (map[string]string, map[string]FlagData, error) { - argContents := make(map[string]string) +func ConfigureArgumentsAndFlags(fileCmd *cobra.Command, metadata *CommandMetadata) (map[string]interface{}, map[string]FlagData, error) { + argContents := make(map[string]interface{}) flagContents := make(map[string]FlagData) requiredArgs := 0 @@ -216,11 +217,12 @@ func ConfigureArgumentsAndFlags(fileCmd *cobra.Command, metadata *CommandMetadat if len(opt.Choices) > 0 { description += fmt.Sprintf(" (Choices: %v)", opt.Choices) } + if opt.File { + description += ` ("-" for read from stdin)` + } if opt.Default != "" { fileCmd.Flags().StringP(flag, shorthand, opt.Default, description) - } else if opt.File { - fileCmd.Flags().StringP(flag, shorthand, "-", description) } else { fileCmd.Flags().StringP(flag, shorthand, "", description) } @@ -234,7 +236,7 @@ func ConfigureArgumentsAndFlags(fileCmd *cobra.Command, metadata *CommandMetadat flagContents[flag] = FlagData{ Choices: opt.Choices, Env: opt.Env, - File: opt.File, + File: [2]bool{opt.File, opt.RawData}, } if helpText != "" && description != "" { @@ -248,12 +250,12 @@ func ConfigureArgumentsAndFlags(fileCmd *cobra.Command, metadata *CommandMetadat // ValidateFlags checks if the flag values passed to the command match any predefined choices and // sets corresponding environment variables if specified in the flag's metadata. If a flag's value // does not match an available choice, an error is returned. -func ValidateFlags(cmd *cobra.Command, flagContents map[string]FlagData, argContents map[string]string) error { +func ValidateFlags(cmd *cobra.Command, flagContents map[string]FlagData, argContents map[string]interface{}, input *slangroom.SlangroomInput) error { for flag, content := range flagContents { var err error value, _ := cmd.Flags().GetString(flag) // Check if value should be read from stdin - if content.File { + if content.File[0] { var fileContent []byte if value == "-" { fileContent, err = io.ReadAll(os.Stdin) @@ -266,7 +268,28 @@ func ValidateFlags(cmd *cobra.Command, flagContents map[string]FlagData, argCont return fmt.Errorf("failed to read file at path %s: %w", value, err) } } - value = strings.TrimSpace(string(fileContent)) + var jsonContent interface{} + if !content.File[1] { + if err := validateJSON(fileContent); err != nil { + return fmt.Errorf("invalid JSON in %s: %w", flag, err) + } + if input.Data != "" { + if input.Data, err = MergeJSON(input.Data, string(fileContent)); err != nil { + log.Println("Error encoding arguments to JSON:", err) + os.Exit(1) + } + } else { + input.Data = string(fileContent) + } + value = "" + } else { + if err = json.Unmarshal(fileContent, &jsonContent); err == nil { + argContents[flag] = jsonContent + value = "" + } else { + value = strings.TrimSpace(string(fileContent)) + } + } } if value == "" && len(content.Env) > 0 { // Try reading the value from the environment variables diff --git a/cmd/utils/utils_test.go b/cmd/utils/utils_test.go index b6440ad..1c0f325 100644 --- a/cmd/utils/utils_test.go +++ b/cmd/utils/utils_test.go @@ -169,6 +169,7 @@ func TestLoadMetadata(t *testing.T) { Env []string `json:"env,omitempty"` Hidden bool `json:"hidden,omitempty"` File bool `json:"file,omitempty"` + RawData bool `json:"rawdata,omitempty"` }{ {Name: "--option1, -o", Description: "Option 1 description", Default: "default1", Choices: []string{"choice1", "choice2"}}, }, @@ -273,6 +274,7 @@ func TestConfigureArgumentsAndFlags(t *testing.T) { Env []string `json:"env,omitempty"` Hidden bool `json:"hidden,omitempty"` File bool `json:"file,omitempty"` + RawData bool `json:"rawdata,omitempty"` }{ {Name: "--flag1", Description: "Test flag", Default: "default_value"}, }, @@ -315,11 +317,11 @@ func TestValidateFlags(t *testing.T) { Env: []string{"TEST_FLAG_ENV_VAR"}, }, "fileFlag": { - File: true, + File: [2]bool{true, true}, }, } - argContents := map[string]string{} + argContents := make(map[string]interface{}) // Test for valid choice and check environment variable setting err := cmd.Flags().Set("flag1", "opt1") @@ -334,7 +336,7 @@ func TestValidateFlags(t *testing.T) { if err != nil { t.Errorf("Unexpected error setting test env variable: %v", err) } - err = ValidateFlags(cmd, flagContents, argContents) + err = ValidateFlags(cmd, flagContents, argContents, nil) if err != nil { t.Errorf("Expected no error, got: %v", err) } @@ -366,7 +368,7 @@ func TestValidateFlags(t *testing.T) { if err != nil { t.Errorf("Unexpected error setting test flag: %v", err) } - err = ValidateFlags(cmd, flagContents, argContents) + err = ValidateFlags(cmd, flagContents, argContents, nil) if err != nil { t.Errorf("Expected no error for stdin read, got: %v", err) } @@ -375,7 +377,7 @@ func TestValidateFlags(t *testing.T) { t.Errorf("Expected 'input from stdin' for fileFlag, got: %v", argContents["fileFlag"]) } - // Test reading from a file path + // Test reading raw data from a file path tmpFile, err := os.CreateTemp("", "testfile") if err != nil { t.Fatalf("error creating temp file: %v", err) @@ -401,19 +403,71 @@ func TestValidateFlags(t *testing.T) { if err != nil { t.Errorf("Unexpected error setting test flag: %v", err) } - err = ValidateFlags(cmd, flagContents, argContents) + err = ValidateFlags(cmd, flagContents, argContents, nil) if err != nil { t.Errorf("Expected no error for file read, got: %v", err) } if argContents["fileFlag"] != "content from file" { t.Errorf("Expected 'content from file' for fileFlag, got: %v", argContents["fileFlag"]) } + //test reading from a json + + cmd.Flags().String("jsonFlag", "", "") + flagContents["jsonFlag"] = FlagData{ + File: [2]bool{true, false}, + } + input := slangroom.SlangroomInput{} + jsonFile, err := os.CreateTemp("", "testfile.json") + if err != nil { + t.Fatalf("error creating temp file: %v", err) + } + defer func() { + err := os.Remove(jsonFile.Name()) + if err != nil { + t.Fatalf("error removing temp file: %v", err) + } + }() + + expected := `{ + "test": { + "name": "Myname", + "data": "somecontent" + }, + "array": [ + "value1", + "value2" + ] +}` + // Write some content to the file + _, err = jsonFile.Write([]byte(expected)) + if err != nil { + t.Fatalf("error writing to temp file: %v", err) + } + err = jsonFile.Close() + if err != nil { + t.Fatalf("error creating temp file: %v", err) + } + + // Set the jsonFlag to the path of the temporary file + err = cmd.Flags().Set("jsonFlag", jsonFile.Name()) + if err != nil { + t.Errorf("Unexpected error setting test flag: %v", err) + } + err = ValidateFlags(cmd, flagContents, argContents, &input) + if err != nil { + t.Errorf("Expected no error for file read, got: %v", err) + } + + if input.Data != expected { + t.Errorf("Expected %s for jsonFlag, got: %v", expected, input.Data) + } + // Test for invalid choice err = cmd.Flags().Set("flag2", "invalid_choice") if err != nil { t.Errorf("Unexpected error setting test flag: %v", err) } - err = ValidateFlags(cmd, flagContents, argContents) + err = ValidateFlags(cmd, flagContents, argContents, nil) if err == nil { t.Errorf("Expected error for invalid flag choice, got: nil") } diff --git a/contracts/test/stdin.metadata.json b/contracts/test/stdin.metadata.json index 158e447..ea88f3e 100644 --- a/contracts/test/stdin.metadata.json +++ b/contracts/test/stdin.metadata.json @@ -3,8 +3,9 @@ "options": [ { "name": "-f, --file ", - "description": "file to read if you pass - the stdin is read instead", - "file": true + "description": "file to read", + "file": true, + "rawdata": true } ] } diff --git a/examples_test.go b/examples_test.go index a84fd7e..5c2fbd6 100644 --- a/examples_test.go +++ b/examples_test.go @@ -133,7 +133,7 @@ func Example_runCmdWithEnvVariable() { func Example_runCmdWithStdinInput() { // Prepare the command to run the slang file cmd1 := exec.Command("cat", "contracts/test/hello.txt") - cmd2 := exec.Command("go", "run", "main.go", "test", "stdin") + cmd2 := exec.Command("go", "run", "main.go", "test", "stdin", "-f", "-") pipe, err := cmd1.StdoutPipe() if err != nil { log.Println("Command execution failed:", err)