Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add possibility to pass directly json file to slangroom input data #11

Merged
merged 6 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,33 @@ The metadata file is automatically read by Gemini to generate appropriate argume
"medium",
"large"
]
}

},
{
"name": "-f, --file <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 (`<arg>`) 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
Expand Down
17 changes: 9 additions & 8 deletions cmd/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -248,15 +249,15 @@ 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)
}
return
}

// 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)
Expand Down
41 changes: 32 additions & 9 deletions cmd/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ 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"`
}

// FlagData contains the necessary data for a given flag
type FlagData struct {
Choices []string
Env []string
File bool
File [2]bool
}

// LoadAdditionalData loads and validates JSON data for additional fields in SlangroomInput.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -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 != "" {
Expand All @@ -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)
Expand All @@ -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
Expand Down
68 changes: 61 additions & 7 deletions cmd/utils/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}},
},
Expand Down Expand Up @@ -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"},
},
Expand Down Expand Up @@ -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")
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
Expand All @@ -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")
}
Expand Down
5 changes: 3 additions & 2 deletions contracts/test/stdin.metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
"options": [
{
"name": "-f, --file <file>",
"description": "file to read if you pass - the stdin is read instead",
"file": true
"description": "file to read",
"file": true,
"rawdata": true
}
]
}
2 changes: 1 addition & 1 deletion examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down