diff --git a/provider/parameter.go b/provider/parameter.go index 2f7dc662..f476e850 100644 --- a/provider/parameter.go +++ b/provider/parameter.go @@ -21,6 +21,25 @@ import ( "golang.org/x/xerrors" ) +type ValidationMode string + +const ( + ValidationModeEnvVar = "CODER_VALIDATION_MODE" + // ValidationModeDefault is used for creating a workspace. It validates the final + // value used for a parameter. Some allowances for invalid options are tolerated, + // as unused options do not affect the final parameter value. The default value + // is also ignored, assuming a value is provided. + ValidationModeDefault ValidationMode = "" + // ValidationModeTemplateImport tolerates empty values, as the value might not be + // available at import. It does not tolerate an invalid default or invalid option + // values. + ValidationModeTemplateImport ValidationMode = "template-import" +) + +var ( + defaultValuePath = cty.Path{cty.GetAttrStep{Name: "default"}} +) + type Option struct { Name string Description string @@ -46,14 +65,13 @@ const ( ) type Parameter struct { - Value string Name string DisplayName string `mapstructure:"display_name"` Description string Type OptionType FormType ParameterFormType Mutable bool - Default string + Default *string Icon string Option []Option Validation []Validation @@ -82,7 +100,6 @@ func parameterDataSource() *schema.Resource { var parameter Parameter err = mapstructure.Decode(struct { - Value interface{} Name interface{} DisplayName interface{} Description interface{} @@ -97,17 +114,22 @@ func parameterDataSource() *schema.Resource { Order interface{} Ephemeral interface{} }{ - Value: rd.Get("value"), Name: rd.Get("name"), DisplayName: rd.Get("display_name"), Description: rd.Get("description"), Type: rd.Get("type"), FormType: rd.Get("form_type"), Mutable: rd.Get("mutable"), - Default: rd.Get("default"), - Icon: rd.Get("icon"), - Option: rd.Get("option"), - Validation: fixedValidation, + Default: func() *string { + if rd.GetRawConfig().AsValueMap()["default"].IsNull() { + return nil + } + val, _ := rd.Get("default").(string) + return &val + }(), + Icon: rd.Get("icon"), + Option: rd.Get("option"), + Validation: fixedValidation, Optional: func() bool { // This hack allows for checking if the "default" field is present in the .tf file. // If "default" is missing or is "null", then it means that this field is required, @@ -122,19 +144,6 @@ func parameterDataSource() *schema.Resource { if err != nil { return diag.Errorf("decode parameter: %s", err) } - var value string - if parameter.Default != "" { - err := valueIsType(parameter.Type, parameter.Default, cty.Path{cty.GetAttrStep{Name: "default"}}) - if err != nil { - return err - } - value = parameter.Default - } - envValue, ok := os.LookupEnv(ParameterEnvironmentVariable(parameter.Name)) - if ok { - value = envValue - } - rd.Set("value", value) if !parameter.Mutable && parameter.Ephemeral { return diag.Errorf("parameter can't be immutable and ephemeral") @@ -144,38 +153,24 @@ func parameterDataSource() *schema.Resource { return diag.Errorf("ephemeral parameter requires the default property") } - // TODO: Should we move this into the Valid() function on - // Parameter? - if len(parameter.Validation) == 1 { - validation := ¶meter.Validation[0] - err = validation.Valid(parameter.Type, value) - if err != nil { - return diag.FromErr(err) - } - } - - // Validate options - _, parameter.FormType, err = ValidateFormType(parameter.Type, len(parameter.Option), parameter.FormType) - if err != nil { - return diag.Diagnostics{ - { - Severity: diag.Error, - Summary: "Invalid form_type for parameter", - Detail: err.Error(), - AttributePath: cty.Path{cty.GetAttrStep{Name: "form_type"}}, - }, - } + var input *string + envValue, ok := os.LookupEnv(ParameterEnvironmentVariable(parameter.Name)) + if ok { + input = &envValue } - // Set the form_type back in case the value was changed. - // Eg via a default. If a user does not specify, a default value - // is used and saved. - rd.Set("form_type", parameter.FormType) - diags := parameter.Valid() + mode := os.Getenv(ValidationModeEnvVar) + value, diags := parameter.Valid(input, ValidationMode(mode)) if diags.HasError() { return diags } + // Always set back the value, as it can be sourced from the default + rd.Set("value", value) + + // Set the form_type as it could have changed in the validation. + rd.Set("form_type", parameter.FormType) + return nil }, Schema: map[string]*schema.Schema{ @@ -389,37 +384,63 @@ func fixValidationResourceData(rawConfig cty.Value, validation interface{}) (int return vArr, nil } -func valueIsType(typ OptionType, value string, attrPath cty.Path) diag.Diagnostics { +func valueIsType(typ OptionType, value string) error { switch typ { case OptionTypeNumber: _, err := strconv.ParseFloat(value, 64) if err != nil { - return diag.Errorf("%q is not a number", value) + return fmt.Errorf("%q is not a number", value) } case OptionTypeBoolean: _, err := strconv.ParseBool(value) if err != nil { - return diag.Errorf("%q is not a bool", value) + return fmt.Errorf("%q is not a bool", value) } case OptionTypeListString: - _, diags := valueIsListString(value, attrPath) - if diags.HasError() { - return diags + _, err := valueIsListString(value) + if err != nil { + return err } case OptionTypeString: // Anything is a string! default: - return diag.Errorf("invalid type %q", typ) + return fmt.Errorf("invalid type %q", typ) } return nil } -func (v *Parameter) Valid() diag.Diagnostics { +func (v *Parameter) Valid(input *string, mode ValidationMode) (string, diag.Diagnostics) { + if mode != ValidationModeDefault && mode != ValidationModeTemplateImport { + return "", diag.Diagnostics{ + { + Severity: diag.Error, + Summary: "Invalid validation mode", + Detail: fmt.Sprintf("validation mode %q is not supported, use %q, or %q", mode, ValidationModeDefault, ValidationModeTemplateImport), + }, + } + } + + var err error + var optionType OptionType + + value := input + if input == nil { + value = v.Default + } + + // TODO: When empty values want to be rejected, uncomment this. + // coder/coder should update to use template import mode first, + // before this is uncommented. + //if value == nil && mode == ValidationModeDefault { + // var empty string + // value = &empty + //} + // optionType might differ from parameter.Type. This is ok, and parameter.Type // should be used for the value type, and optionType for options. - optionType, _, err := ValidateFormType(v.Type, len(v.Option), v.FormType) + optionType, v.FormType, err = ValidateFormType(v.Type, len(v.Option), v.FormType) if err != nil { - return diag.Diagnostics{ + return "", diag.Diagnostics{ { Severity: diag.Error, Summary: "Invalid form_type for parameter", @@ -429,53 +450,151 @@ func (v *Parameter) Valid() diag.Diagnostics { } } - optionNames := map[string]any{} - optionValues := map[string]any{} - if len(v.Option) > 0 { - for _, option := range v.Option { - _, exists := optionNames[option.Name] - if exists { - return diag.Diagnostics{{ + optionValues, diags := v.ValidOptions(optionType, mode) + if diags.HasError() { + return "", diags + } + + if mode == ValidationModeTemplateImport && v.Default != nil { + // Template import should validate the default value. + err := valueIsType(v.Type, *v.Default) + if err != nil { + return "", diag.Diagnostics{ + { + Severity: diag.Error, + Summary: fmt.Sprintf("Default value is not of type %q", v.Type), + Detail: err.Error(), + AttributePath: defaultValuePath, + }, + } + } + + d := v.validValue(*v.Default, optionType, optionValues, defaultValuePath) + if d.HasError() { + return "", d + } + } + + // TODO: This is a bit of a hack. The current behavior states if validation + // is given, then apply validation to unset values. + // This should be removed, and all values should be validated. Meaning + // value == nil should not be accepted in the first place. + if len(v.Validation) > 0 && value == nil { + empty := "" + value = &empty + } + + // Value is only validated if it is set. If it is unset, validation + // is skipped. + if value != nil { + d := v.validValue(*value, optionType, optionValues, cty.Path{}) + if d.HasError() { + return "", d + } + + err = valueIsType(v.Type, *value) + if err != nil { + return "", diag.Diagnostics{ + { Severity: diag.Error, - Summary: "Option names must be unique.", - Detail: fmt.Sprintf("multiple options found with the same name %q", option.Name), + Summary: fmt.Sprintf("Parameter value is not of type %q", v.Type), + Detail: err.Error(), }, - } } - _, exists = optionValues[option.Value] - if exists { - return diag.Diagnostics{ - { - Severity: diag.Error, - Summary: "Option values must be unique.", - Detail: fmt.Sprintf("multiple options found with the same value %q", option.Value), - }, + } + } + + if value == nil { + // The previous behavior is to always write an empty string + return "", nil + } + + return *value, nil +} + +func (v *Parameter) ValidOptions(optionType OptionType, mode ValidationMode) (map[string]struct{}, diag.Diagnostics) { + optionNames := map[string]struct{}{} + optionValues := map[string]struct{}{} + + var diags diag.Diagnostics + for _, option := range v.Option { + _, exists := optionNames[option.Name] + if exists { + return nil, diag.Diagnostics{{ + Severity: diag.Error, + Summary: "Option names must be unique.", + Detail: fmt.Sprintf("multiple options found with the same name %q", option.Name), + }} + } + + _, exists = optionValues[option.Value] + if exists { + return nil, diag.Diagnostics{{ + Severity: diag.Error, + Summary: "Option values must be unique.", + Detail: fmt.Sprintf("multiple options found with the same value %q", option.Value), + }} + } + + err := valueIsType(optionType, option.Value) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: fmt.Sprintf("Option %q with value=%q is not of type %q", option.Name, option.Value, optionType), + Detail: err.Error(), + }) + continue + } + optionValues[option.Value] = struct{}{} + optionNames[option.Name] = struct{}{} + + if mode == ValidationModeTemplateImport { + opDiags := v.validValue(option.Value, optionType, nil, cty.Path{}) + if opDiags.HasError() { + for i := range opDiags { + opDiags[i].Summary = fmt.Sprintf("Option %q: %s", option.Name, opDiags[i].Summary) } + diags = append(diags, opDiags...) } - diags := valueIsType(optionType, option.Value, cty.Path{}) - if diags.HasError() { - return diags - } - optionValues[option.Value] = nil - optionNames[option.Name] = nil } } - if v.Default != "" && len(v.Option) > 0 { + if diags.HasError() { + return nil, diags + } + return optionValues, nil +} + +func (v *Parameter) validValue(value string, optionType OptionType, optionValues map[string]struct{}, path cty.Path) diag.Diagnostics { + // name is used for constructing more precise error messages. + name := "Value" + if path.Equals(defaultValuePath) { + name = "Default value" + } + + // First validate if the value is a valid option + if len(optionValues) > 0 { if v.Type == OptionTypeListString && optionType == OptionTypeString { // If the type is list(string) and optionType is string, we have // to ensure all elements of the default exist as options. - defaultValues, diags := valueIsListString(v.Default, cty.Path{cty.GetAttrStep{Name: "default"}}) - if diags.HasError() { - return diags + listValues, err := valueIsListString(value) + if err != nil { + return diag.Diagnostics{ + { + Severity: diag.Error, + Summary: "When using list(string) type, value must be a json encoded list of strings", + Detail: err.Error(), + AttributePath: defaultValuePath, + }, + } } // missing is used to construct a more helpful error message var missing []string - for _, defaultValue := range defaultValues { - _, defaultIsValid := optionValues[defaultValue] - if !defaultIsValid { - missing = append(missing, defaultValue) + for _, listValue := range listValues { + _, isValid := optionValues[listValue] + if !isValid { + missing = append(missing, listValue) } } @@ -483,30 +602,49 @@ func (v *Parameter) Valid() diag.Diagnostics { return diag.Diagnostics{ { Severity: diag.Error, - Summary: "Default values must be a valid option", + Summary: fmt.Sprintf("%ss must be a valid option", name), Detail: fmt.Sprintf( - "default value %q is not a valid option, values %q are missing from the options", - v.Default, strings.Join(missing, ", "), + "%s %q is not a valid option, values %q are missing from the options", + name, value, strings.Join(missing, ", "), ), - AttributePath: cty.Path{cty.GetAttrStep{Name: "default"}}, + AttributePath: defaultValuePath, }, } } } else { - _, defaultIsValid := optionValues[v.Default] - if !defaultIsValid { + _, isValid := optionValues[value] + if !isValid { + extra := "" + if value == "" { + extra = ". The value is empty, did you forget to set it with a default or from user input?" + } return diag.Diagnostics{ { Severity: diag.Error, - Summary: "Default value must be a valid option", - Detail: fmt.Sprintf("the value %q must be defined as one of options", v.Default), - AttributePath: cty.Path{cty.GetAttrStep{Name: "default"}}, + Summary: fmt.Sprintf("%s must be a valid option%s", name, extra), + Detail: fmt.Sprintf("the value %q must be defined as one of options", value), + AttributePath: path, }, } } } } + if len(v.Validation) == 1 { + validCheck := &v.Validation[0] + err := validCheck.Valid(v.Type, value) + if err != nil { + return diag.Diagnostics{ + { + Severity: diag.Error, + Summary: fmt.Sprintf("Invalid parameter %s according to 'validation' block", strings.ToLower(name)), + Detail: err.Error(), + AttributePath: path, + }, + } + } + } + return nil } @@ -570,18 +708,11 @@ func (v *Validation) Valid(typ OptionType, value string) error { return nil } -func valueIsListString(value string, path cty.Path) ([]string, diag.Diagnostics) { +func valueIsListString(value string) ([]string, error) { var items []string err := json.Unmarshal([]byte(value), &items) if err != nil { - return nil, diag.Diagnostics{ - { - Severity: diag.Error, - Summary: "When using list(string) type, value must be a json encoded list of strings", - Detail: fmt.Sprintf("value %q is not a valid list of strings", value), - AttributePath: path, - }, - } + return nil, fmt.Errorf("value %q is not a valid list of strings", value) } return items, nil } diff --git a/provider/parameter_test.go b/provider/parameter_test.go index 32877c2b..b2558cb5 100644 --- a/provider/parameter_test.go +++ b/provider/parameter_test.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/coder/terraform-provider-coder/v2/provider" @@ -688,6 +689,168 @@ data "coder_parameter" "region" { } } +func TestParameterValidation(t *testing.T) { + t.Parallel() + opts := func(vals ...string) []provider.Option { + options := make([]provider.Option, 0, len(vals)) + for _, val := range vals { + options = append(options, provider.Option{ + Name: val, + Value: val, + }) + } + return options + } + + for _, tc := range []struct { + Name string + Parameter provider.Parameter + Value string + ExpectError *regexp.Regexp + }{ + { + Name: "ValidStringParameter", + Parameter: provider.Parameter{ + Type: "string", + }, + Value: "alpha", + }, + // Test invalid states + { + Name: "InvalidFormType", + Parameter: provider.Parameter{ + Type: "string", + Option: opts("alpha", "bravo", "charlie"), + FormType: provider.ParameterFormTypeSlider, + }, + Value: "alpha", + ExpectError: regexp.MustCompile("Invalid form_type for parameter"), + }, + { + Name: "NotInOptions", + Parameter: provider.Parameter{ + Type: "string", + Option: opts("alpha", "bravo", "charlie"), + }, + Value: "delta", // not in option set + ExpectError: regexp.MustCompile("Value must be a valid option"), + }, + { + Name: "NumberNotInOptions", + Parameter: provider.Parameter{ + Type: "number", + Option: opts("1", "2", "3"), + }, + Value: "0", // not in option set + ExpectError: regexp.MustCompile("Value must be a valid option"), + }, + { + Name: "NonUniqueOptionNames", + Parameter: provider.Parameter{ + Type: "string", + Option: opts("alpha", "alpha"), + }, + Value: "alpha", + ExpectError: regexp.MustCompile("Option names must be unique"), + }, + { + Name: "NonUniqueOptionValues", + Parameter: provider.Parameter{ + Type: "string", + Option: []provider.Option{ + {Name: "Alpha", Value: "alpha"}, + {Name: "AlphaAgain", Value: "alpha"}, + }, + }, + Value: "alpha", + ExpectError: regexp.MustCompile("Option values must be unique"), + }, + { + Name: "IncorrectValueTypeOption", + Parameter: provider.Parameter{ + Type: "number", + Option: opts("not-a-number"), + }, + Value: "5", + ExpectError: regexp.MustCompile("is not a number"), + }, + { + Name: "IncorrectValueType", + Parameter: provider.Parameter{ + Type: "number", + }, + Value: "not-a-number", + ExpectError: regexp.MustCompile("Parameter value is not of type \"number\""), + }, + { + Name: "NotListStringDefault", + Parameter: provider.Parameter{ + Type: "list(string)", + Default: ptr("not-a-list"), + }, + ExpectError: regexp.MustCompile("not a valid list of strings"), + }, + { + Name: "NotListStringDefault", + Parameter: provider.Parameter{ + Type: "list(string)", + }, + Value: "not-a-list", + ExpectError: regexp.MustCompile("not a valid list of strings"), + }, + { + Name: "DefaultListStringNotInOptions", + Parameter: provider.Parameter{ + Type: "list(string)", + Default: ptr(`["red", "yellow", "black"]`), + Option: opts("red", "blue", "green"), + FormType: provider.ParameterFormTypeMultiSelect, + }, + Value: `["red", "yellow", "black"]`, + ExpectError: regexp.MustCompile("is not a valid option, values \"yellow, black\" are missing from the options"), + }, + { + Name: "ListStringNotInOptions", + Parameter: provider.Parameter{ + Type: "list(string)", + Default: ptr(`["red"]`), + Option: opts("red", "blue", "green"), + FormType: provider.ParameterFormTypeMultiSelect, + }, + Value: `["red", "yellow", "black"]`, + ExpectError: regexp.MustCompile("is not a valid option, values \"yellow, black\" are missing from the options"), + }, + { + Name: "InvalidMiniumum", + Parameter: provider.Parameter{ + Type: "number", + Default: ptr("5"), + Validation: []provider.Validation{{ + Min: 10, + Error: "must be greater than 10", + }}, + }, + ExpectError: regexp.MustCompile("must be greater than 10"), + }, + } { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + value := &tc.Value + _, diags := tc.Parameter.Valid(value, provider.ValidationModeDefault) + if tc.ExpectError != nil { + require.True(t, diags.HasError()) + errMsg := fmt.Sprintf("%+v", diags[0]) // close enough + require.Truef(t, tc.ExpectError.MatchString(errMsg), "got: %s", errMsg) + } else { + if !assert.False(t, diags.HasError()) { + t.Logf("got: %+v", diags[0]) + } + } + }) + } +} + // TestParameterValidationEnforcement tests various parameter states and the // validation enforcement that should be applied to them. The table is described // by a markdown table. This is done so that the test cases can be more easily @@ -703,10 +866,6 @@ func TestParameterValidationEnforcement(t *testing.T) { // - Validation logic does not apply to the default if a value is given // - [NumIns/DefInv] So the default can be invalid if an input value is valid. // The value is therefore not really optional, but it is marked as such. - // - [NumInsNotOptsVal | NumsInsNotOpts] values do not need to be in the option set? - // - [NumInsNotNum] number params do not require the value to be a number - // - [LStrInsNotList] list(string) do not require the value to be a list(string) - // - Same with [MulInsNotListOpts] table, err := os.ReadFile("testdata/parameter_table.md") require.NoError(t, err) @@ -719,7 +878,8 @@ func TestParameterValidationEnforcement(t *testing.T) { Validation *provider.Validation OutputValue string Optional bool - Error *regexp.Regexp + CreateError *regexp.Regexp + ImportError *regexp.Regexp } rows := make([]row, 0) @@ -750,6 +910,19 @@ func TestParameterValidationEnforcement(t *testing.T) { t.Fatalf("failed to parse error column %q: %v", columns[9], err) } } + + var imerr *regexp.Regexp + if columns[10] != "" { + if columns[10] == "=" { + imerr = rerr + } else { + imerr, err = regexp.Compile(columns[10]) + if err != nil { + t.Fatalf("failed to parse error column %q: %v", columns[10], err) + } + } + } + var options []string if columns[4] != "" { options = strings.Split(columns[4], ",") @@ -796,7 +969,8 @@ func TestParameterValidationEnforcement(t *testing.T) { Validation: validation, OutputValue: columns[7], Optional: optional, - Error: rerr, + CreateError: rerr, + ImportError: imerr, }) } @@ -815,9 +989,9 @@ func TestParameterValidationEnforcement(t *testing.T) { t.Setenv(provider.ParameterEnvironmentVariable("parameter"), row.InputValue) } - if row.Error != nil { + if row.CreateError != nil && row.ImportError != nil { if row.OutputValue != "" { - t.Errorf("output value %q should not be set if error is set", row.OutputValue) + t.Errorf("output value %q should not be set if both errors are set", row.OutputValue) } } @@ -861,42 +1035,56 @@ func TestParameterValidationEnforcement(t *testing.T) { cfg.WriteString("}\n") - resource.Test(t, resource.TestCase{ - ProviderFactories: coderFactory(), - IsUnitTest: true, - Steps: []resource.TestStep{{ - Config: cfg.String(), - ExpectError: row.Error, - Check: func(state *terraform.State) error { - require.Len(t, state.Modules, 1) - require.Len(t, state.Modules[0].Resources, 1) - param := state.Modules[0].Resources["data.coder_parameter.parameter"] - require.NotNil(t, param) + for _, mode := range []provider.ValidationMode{provider.ValidationModeDefault, provider.ValidationModeTemplateImport} { + name := string(mode) + if mode == provider.ValidationModeDefault { + name = "create" + } + t.Run(name, func(t *testing.T) { + t.Setenv("CODER_VALIDATION_MODE", string(mode)) + rerr := row.CreateError + if mode == provider.ValidationModeTemplateImport { + rerr = row.ImportError + } + + resource.Test(t, resource.TestCase{ + ProviderFactories: coderFactory(), + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: cfg.String(), + ExpectError: rerr, + Check: func(state *terraform.State) error { + require.Len(t, state.Modules, 1) + require.Len(t, state.Modules[0].Resources, 1) + param := state.Modules[0].Resources["data.coder_parameter.parameter"] + require.NotNil(t, param) - if row.Default == "" { - _, ok := param.Primary.Attributes["default"] - require.False(t, ok, "default should not be set") - } else { - require.Equal(t, strings.Trim(row.Default, `"`), param.Primary.Attributes["default"]) - } + if row.Default == "" { + _, ok := param.Primary.Attributes["default"] + require.False(t, ok, "default should not be set") + } else { + require.Equal(t, strings.Trim(row.Default, `"`), param.Primary.Attributes["default"]) + } - if row.OutputValue == "" { - _, ok := param.Primary.Attributes["value"] - require.False(t, ok, "output value should not be set") - } else { - require.Equal(t, strings.Trim(row.OutputValue, `"`), param.Primary.Attributes["value"]) - } + if row.OutputValue == "" { + _, ok := param.Primary.Attributes["value"] + require.False(t, ok, "output value should not be set") + } else { + require.Equal(t, strings.Trim(row.OutputValue, `"`), param.Primary.Attributes["value"]) + } - for key, expected := range map[string]string{ - "optional": strconv.FormatBool(row.Optional), - } { - require.Equal(t, expected, param.Primary.Attributes[key], "optional") - } + for key, expected := range map[string]string{ + "optional": strconv.FormatBool(row.Optional), + } { + require.Equal(t, expected, param.Primary.Attributes[key], "optional") + } - return nil - }, - }}, - }) + return nil + }, + }}, + }) + }) + } }) } } @@ -1096,3 +1284,7 @@ func TestParameterWithManyOptions(t *testing.T) { }}, }) } + +func ptr[T any](v T) *T { + return &v +} diff --git a/provider/testdata/parameter_table.md b/provider/testdata/parameter_table.md index f7645efa..cf51b8cd 100644 --- a/provider/testdata/parameter_table.md +++ b/provider/testdata/parameter_table.md @@ -1,79 +1,80 @@ -| Name | Type | Input | Default | Options | Validation | -> | Output | Optional | Error | -|----------------------|---------------|-----------|---------|-------------------|------------|----|--------|----------|--------------| -| | Empty Vals | | | | | | | | | -| Empty | string,number | | | | | | "" | false | | -| EmptyDupeOps | string,number | | | 1,1,1 | | | | | unique | -| EmptyList | list(string) | | | | | | "" | false | | -| EmptyListDupeOpts | list(string) | | | ["a"],["a"] | | | | | unique | -| EmptyMulti | tag-select | | | | | | "" | false | | -| EmptyOpts | string,number | | | 1,2,3 | | | "" | false | | -| EmptyRegex | string | | | | world | | | | regex error | -| EmptyMin | number | | | | 1-10 | | | | 1 < < 10 | -| EmptyMinOpt | number | | | 1,2,3 | 2-5 | | | | 2 < < 5 | -| EmptyRegexOpt | string | | | "hello","goodbye" | goodbye | | | | regex error | -| EmptyRegexOk | string | | | | .* | | "" | false | | -| | | | | | | | | | | -| | Default Set | No inputs | | | | | | | | -| NumDef | number | | 5 | | | | 5 | true | | -| NumDefVal | number | | 5 | | 3-7 | | 5 | true | | -| NumDefInv | number | | 5 | | 10- | | | | 10 < 5 < 0 | -| NumDefOpts | number | | 5 | 1,3,5,7 | 2-6 | | 5 | true | | -| NumDefNotOpts | number | | 5 | 1,3,7,9 | 2-6 | | | | valid option | -| NumDefInvOpt | number | | 5 | 1,3,5,7 | 6-10 | | | | 6 < 5 < 10 | -| NumDefNotNum | number | | a | | | | | | a number | -| NumDefOptsNotNum | number | | 1 | 1,a,2 | | | | | a number | -| | | | | | | | | | | -| StrDef | string | | hello | | | | hello | true | | -| StrDefInv | string | | hello | | world | | | | regex error | -| StrDefOpts | string | | a | a,b,c | | | a | true | | -| StrDefNotOpts | string | | a | b,c,d | | | | | valid option | -| StrDefValOpts | string | | a | a,b,c,d,e,f | [a-c] | | a | true | | -| StrDefInvOpt | string | | d | a,b,c,d,e,f | [a-c] | | | | regex error | -| | | | | | | | | | | -| LStrDef | list(string) | | ["a"] | | | | ["a"] | true | | -| LStrDefOpts | list(string) | | ["a"] | ["a"], ["b"] | | | ["a"] | true | | -| LStrDefNotOpts | list(string) | | ["a"] | ["b"], ["c"] | | | | | valid option | -| | | | | | | | | | | -| MulDef | tag-select | | ["a"] | | | | ["a"] | true | | -| MulDefOpts | multi-select | | ["a"] | a,b | | | ["a"] | true | | -| MulDefNotOpts | multi-select | | ["a"] | b,c | | | | | valid option | -| | | | | | | | | | | -| | Input Vals | | | | | | | | | -| NumIns | number | 3 | | | | | 3 | false | | -| NumInsNotNum | number | a | | | | | a | false | | -| NumInsNotNumInv | number | a | | | 1-3 | | | | 1 < a < 3 | -| NumInsDef | number | 3 | 5 | | | | 3 | true | | -| NumIns/DefInv | number | 3 | 5 | | 1-3 | | 3 | true | | -| NumIns=DefInv | number | 5 | 5 | | 1-3 | | | | 1 < 5 < 3 | -| NumInsOpts | number | 3 | 5 | 1,2,3,4,5 | 1-3 | | 3 | true | | -| NumInsNotOptsVal | number | 3 | 5 | 1,2,4,5 | 1-3 | | 3 | true | | -| NumInsNotOptsInv | number | 3 | 5 | 1,2,4,5 | 1-2 | | | true | 1 < 3 < 2 | -| NumInsNotOpts | number | 3 | 5 | 1,2,4,5 | | | 3 | true | | -| NumInsNotOpts/NoDef | number | 3 | | 1,2,4,5 | | | 3 | false | | -| | | | | | | | | | | -| StrIns | string | c | | | | | c | false | | -| StrInsDupeOpts | string | c | | a,b,c,c | | | | | unique | -| StrInsDef | string | c | e | | | | c | true | | -| StrIns/DefInv | string | c | e | | [a-c] | | c | true | | -| StrIns=DefInv | string | e | e | | [a-c] | | | | regex error | -| StrInsOpts | string | c | e | a,b,c,d,e | [a-c] | | c | true | | -| StrInsNotOptsVal | string | c | e | a,b,d,e | [a-c] | | c | true | | -| StrInsNotOptsInv | string | c | e | a,b,d,e | [a-b] | | | | regex error | -| StrInsNotOpts | string | c | e | a,b,d,e | | | c | true | | -| StrInsNotOpts/NoDef | string | c | | a,b,d,e | | | c | false | | -| StrInsBadVal | string | c | | a,b,c,d,e | 1-10 | | | | min cannot | -| | | | | | | | | | | -| | list(string) | | | | | | | | | -| LStrIns | list(string) | ["c"] | | | | | ["c"] | false | | -| LStrInsNotList | list(string) | c | | | | | c | false | | -| LStrInsDef | list(string) | ["c"] | ["e"] | | | | ["c"] | true | | -| LStrIns/DefInv | list(string) | ["c"] | ["e"] | | [a-c] | | | | regex cannot | -| LStrInsOpts | list(string) | ["c"] | ["e"] | ["c"],["d"],["e"] | | | ["c"] | true | | -| LStrInsNotOpts | list(string) | ["c"] | ["e"] | ["d"],["e"] | | | ["c"] | true | | -| LStrInsNotOpts/NoDef | list(string) | ["c"] | | ["d"],["e"] | | | ["c"] | false | | -| | | | | | | | | | | -| MulInsOpts | multi-select | ["c"] | ["e"] | c,d,e | | | ["c"] | true | | -| MulInsNotListOpts | multi-select | c | ["e"] | c,d,e | | | c | true | | -| MulInsNotOpts | multi-select | ["c"] | ["e"] | d,e | | | ["c"] | true | | -| MulInsNotOpts/NoDef | multi-select | ["c"] | | d,e | | | ["c"] | false | | -| MulInsInvOpts | multi-select | ["c"] | ["e"] | c,d,e | [a-c] | | | | regex cannot | \ No newline at end of file +| Name | Type | Input | Default | Options | Validation | -> | Output | Optional | ErrorCreate | ErrorImport | +|----------------------|---------------|-----------|---------|-------------------|------------|----|--------|----------|--------------|---------------| +| | Empty Vals | | | | | | | | | | +| Empty | string,number | | | | | | "" | false | | | +| EmptyDupeOps | string,number | | | 1,1,1 | | | | | unique | = | +| EmptyList | list(string) | | | | | | "" | false | | | +| EmptyListDupeOpts | list(string) | | | ["a"],["a"] | | | | | unique | = | +| EmptyMulti | tag-select | | | | | | "" | false | | | +| EmptyOpts | string,number | | | 1,2,3 | | | "" | false | | | +| EmptyRegex | string | | | | world | | | | regex error | = | +| EmptyMin | number | | | | 1-10 | | | | 1 < < 10 | = | +| EmptyMinOpt | number | | | 1,2,3 | 2-5 | | | | 2 < < 5 | 2 < 1 < 5 | +| EmptyRegexOpt | string | | | "hello","goodbye" | goodbye | | | | regex error | regex error | +| EmptyRegexOk | string | | | | .* | | "" | false | | | +| | | | | | | | | | | | +| | Default Set | No inputs | | | | | | | | | +| NumDef | number | | 5 | | | | 5 | true | | | +| NumDefVal | number | | 5 | | 3-7 | | 5 | true | | | +| NumDefInv | number | | 5 | | 10- | | | | 10 < 5 < 0 | = | +| NumDefOpts | number | | 5 | 1,3,5,7 | 2-6 | | 5 | true | | 2 < 1 < 6 | +| NumDefNotOpts | number | | 5 | 1,3,7,9 | 2-6 | | | | valid option | 2 < 1 < 6 | +| NumDefInvOpt | number | | 5 | 1,3,5,7 | 6-10 | | | | 6 < 5 < 10 | = | +| NumDefNotNum | number | | a | | | | | | a number | = | +| NumDefOptsNotNum | number | | 1 | 1,a,2 | | | | | a number | = | +| | | | | | | | | | | | +| StrDef | string | | hello | | | | hello | true | | | +| StrDefInv | string | | hello | | world | | | | regex error | = | +| StrDefOpts | string | | a | a,b,c | | | a | true | | | +| StrDefNotOpts | string | | a | b,c,d | | | | | valid option | = | +| StrDefValOpts | string | | a | a,b,c,d,e,f | [a-c] | | a | true | | value "d" | +| StrDefInvOpt | string | | d | a,b,c,d,e,f | [a-c] | | | | regex error | = | +| | | | | | | | | | | | +| LStrDef | list(string) | | ["a"] | | | | ["a"] | true | | | +| LStrDefOpts | list(string) | | ["a"] | ["a"], ["b"] | | | ["a"] | true | | | +| LStrDefNotOpts | list(string) | | ["a"] | ["b"], ["c"] | | | | | valid option | = | +| | | | | | | | | | | | +| MulDef | tag-select | | ["a"] | | | | ["a"] | true | | | +| MulDefOpts | multi-select | | ["a"] | a,b | | | ["a"] | true | | | +| MulDefNotOpts | multi-select | | ["a"] | b,c | | | | | valid option | = | +| | | | | | | | | | | | +| | Input Vals | | | | | | | | | | +| NumIns | number | 3 | | | | | 3 | false | | | +| NumInsOptsNaN | number | 3 | 5 | a,1,2,3,4,5 | 1-3 | | | | a number | = | +| NumInsNotNum | number | a | | | | | a | false | | = | +| NumInsNotNumInv | number | a | | | 1-3 | | | | 1 < a < 3 | = | +| NumInsDef | number | 3 | 5 | | | | 3 | true | | | +| NumIns/DefInv | number | 3 | 5 | | 1-3 | | 3 | true | | 1 < 5 < 3 | +| NumIns=DefInv | number | 5 | 5 | | 1-3 | | | | 1 < 5 < 3 | = | +| NumInsOpts | number | 3 | 5 | 1,2,3,4,5 | 1-3 | | 3 | true | | 1 < 5 < 3 | +| NumInsNotOptsVal | number | 3 | 5 | 1,2,4,5 | 1-3 | | 3 | true | | 1 < 4 < 3 | +| NumInsNotOptsInv | number | 3 | 5 | 1,2,4,5 | 1-2 | | | true | 1 < 3 < 2 | 1 < 4 < 2 | +| NumInsNotOpts | number | 3 | 5 | 1,2,4,5 | | | 3 | true | | = | +| NumInsNotOpts/NoDef | number | 3 | | 1,2,4,5 | | | 3 | false | | = | +| | | | | | | | | | | | +| StrIns | string | c | | | | | c | false | | | +| StrInsDupeOpts | string | c | | a,b,c,c | | | | | unique | = | +| StrInsDef | string | c | e | | | | c | true | | | +| StrIns/DefInv | string | c | e | | [a-c] | | c | true | | default value | +| StrIns=DefInv | string | e | e | | [a-c] | | | | regex error | = | +| StrInsOpts | string | c | e | a,b,c,d,e | [a-c] | | c | true | | value "d" | +| StrInsNotOptsVal | string | c | e | a,b,d,e | [a-c] | | c | true | | value "d" | +| StrInsNotOptsInv | string | c | e | a,b,d,e | [a-b] | | | | regex error | regex error | +| StrInsNotOpts | string | c | e | a,b,d,e | | | c | true | | = | +| StrInsNotOpts/NoDef | string | c | | a,b,d,e | | | c | false | | = | +| StrInsBadVal | string | c | | a,b,c,d,e | 1-10 | | | | min cannot | = | +| | | | | | | | | | | | +| | list(string) | | | | | | | | | | +| LStrIns | list(string) | ["c"] | | | | | ["c"] | false | | | +| LStrInsNotList | list(string) | c | | | | | c | false | | = | +| LStrInsDef | list(string) | ["c"] | ["e"] | | | | ["c"] | true | | | +| LStrIns/DefInv | list(string) | ["c"] | ["e"] | | [a-c] | | | | regex cannot | = | +| LStrInsOpts | list(string) | ["c"] | ["e"] | ["c"],["d"],["e"] | | | ["c"] | true | | | +| LStrInsNotOpts | list(string) | ["c"] | ["e"] | ["d"],["e"] | | | ["c"] | true | | = | +| LStrInsNotOpts/NoDef | list(string) | ["c"] | | ["d"],["e"] | | | ["c"] | false | | = | +| | | | | | | | | | | | +| MulInsOpts | multi-select | ["c"] | ["e"] | c,d,e | | | ["c"] | true | | | +| MulInsNotListOpts | multi-select | c | ["e"] | c,d,e | | | c | true | | = | +| MulInsNotOpts | multi-select | ["c"] | ["e"] | d,e | | | ["c"] | true | | = | +| MulInsNotOpts/NoDef | multi-select | ["c"] | | d,e | | | ["c"] | false | | = | +| MulInsInvOpts | multi-select | ["c"] | ["e"] | c,d,e | [a-c] | | | | regex cannot | = | \ No newline at end of file