diff --git a/provider/parameter.go b/provider/parameter.go index 2f7dc662..fd484578 100644 --- a/provider/parameter.go +++ b/provider/parameter.go @@ -21,6 +21,10 @@ import ( "golang.org/x/xerrors" ) +var ( + defaultValuePath = cty.Path{cty.GetAttrStep{Name: "default"}} +) + type Option struct { Name string Description string @@ -46,14 +50,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 +85,6 @@ func parameterDataSource() *schema.Resource { var parameter Parameter err = mapstructure.Decode(struct { - Value interface{} Name interface{} DisplayName interface{} Description interface{} @@ -97,17 +99,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 +129,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 +138,25 @@ 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() + value, diags := parameter.ValidateInput(input) 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 if it was unset, a default form_type will be updated on + // the parameter struct. Always set back the updated form_type to be more + // specific than the default empty string. + rd.Set("form_type", parameter.FormType) + return nil }, Schema: map[string]*schema.Schema{ @@ -389,37 +370,49 @@ 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) ValidateInput(input *string) (string, diag.Diagnostics) { + var err error + var optionType OptionType + + valuePath := cty.Path{} + value := input + if input == nil { + value = v.Default + if v.Default != nil { + valuePath = defaultValuePath + } + } + // 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 +422,120 @@ 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{{ - 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 diag.Diagnostics{ - { - Severity: diag.Error, - Summary: "Option values must be unique.", - Detail: fmt.Sprintf("multiple options found with the same value %q", option.Value), - }, - } - } - diags := valueIsType(optionType, option.Value, cty.Path{}) - if diags.HasError() { - return diags - } - optionValues[option.Value] = nil - optionNames[option.Name] = nil + optionValues, diags := v.ValidOptions(optionType) + if diags.HasError() { + return "", diags + } + + // TODO: This is a bit of a hack. The current behavior states if validation + // is given, then apply validation to unset values. + // value == nil should not be accepted in the first place. + // To fix this, value should be coerced to an empty string + // if it is nil. Then let the validation logic always apply. + if len(v.Validation) == 0 && value == nil { + return "", nil + } + + // forcedValue ensures the value is not-nil. + var forcedValue string + if value != nil { + forcedValue = *value + } + + d := v.validValue(forcedValue, optionType, optionValues, valuePath) + if d.HasError() { + return "", d + } + + err = valueIsType(v.Type, forcedValue) + if err != nil { + return "", diag.Diagnostics{ + { + Severity: diag.Error, + Summary: fmt.Sprintf("Parameter value is not of type %q", v.Type), + Detail: err.Error(), + }, } } - if v.Default != "" && len(v.Option) > 0 { + return forcedValue, nil +} + +func (v *Parameter) ValidOptions(optionType OptionType) (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{}{} + + // Option values are assumed to be valid. Do not call validValue on them. + } + + if diags != nil && 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 + // to ensure all elements of the value exist as options. + 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: path, + }, + } } // 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 +543,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: path, }, } } } 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 +649,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..21842b6a 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" @@ -84,6 +85,7 @@ func TestParameter(t *testing.T) { data "coder_parameter" "region" { name = "Region" type = "number" + default = 1 option { name = "1" value = "1" @@ -101,6 +103,7 @@ func TestParameter(t *testing.T) { data "coder_parameter" "region" { name = "Region" type = "string" + default = "1" option { name = "1" value = "1" @@ -688,6 +691,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.ValidateInput(value) + 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 +868,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 +880,7 @@ func TestParameterValidationEnforcement(t *testing.T) { Validation *provider.Validation OutputValue string Optional bool - Error *regexp.Regexp + CreateError *regexp.Regexp } rows := make([]row, 0) @@ -750,6 +911,7 @@ func TestParameterValidationEnforcement(t *testing.T) { t.Fatalf("failed to parse error column %q: %v", columns[9], err) } } + var options []string if columns[4] != "" { options = strings.Split(columns[4], ",") @@ -796,7 +958,7 @@ func TestParameterValidationEnforcement(t *testing.T) { Validation: validation, OutputValue: columns[7], Optional: optional, - Error: rerr, + CreateError: rerr, }) } @@ -815,10 +977,8 @@ func TestParameterValidationEnforcement(t *testing.T) { t.Setenv(provider.ParameterEnvironmentVariable("parameter"), row.InputValue) } - if row.Error != nil { - if row.OutputValue != "" { - t.Errorf("output value %q should not be set if error is set", row.OutputValue) - } + if row.CreateError != nil && row.OutputValue != "" { + t.Errorf("output value %q should not be set if both errors are set", row.OutputValue) } var cfg strings.Builder @@ -860,13 +1020,12 @@ 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, + ExpectError: row.CreateError, Check: func(state *terraform.State) error { require.Len(t, state.Modules, 1) require.Len(t, state.Modules[0].Resources, 1) @@ -1096,3 +1255,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..3df16f06 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 | +|----------------------|---------------|-----------|---------|-------------------|------------|----|--------|----------|-----------------| +| | 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 | | | | valid option | +| EmptyRegexOpt | string | | | "hello","goodbye" | goodbye | | | | valid option | +| 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 | | | | | | type "number" | +| NumDefOptsNotNum | number | | 1 | 1,a,2 | | | | | type "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 | | +| NumInsOptsNaN | number | 3 | 5 | a,1,2,3,4,5 | 1-3 | | | | type "number" | +| NumInsNotNum | number | a | | | | | | | type "number" | +| 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 | | | | valid option | +| NumInsNotOptsInv | number | 3 | 5 | 1,2,4,5 | 1-2 | | | true | valid option | +| NumInsNotOpts | number | 3 | 5 | 1,2,4,5 | | | | | valid option | +| NumInsNotOpts/NoDef | number | 3 | | 1,2,4,5 | | | | | valid option | +| | | | | | | | | | | +| 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] | | | | valid option | +| StrInsNotOptsInv | string | c | e | a,b,d,e | [a-b] | | | | valid option | +| StrInsNotOpts | string | c | e | a,b,d,e | | | | | valid option | +| StrInsNotOpts/NoDef | string | c | | a,b,d,e | | | | | valid option | +| StrInsBadVal | string | c | | a,b,c,d,e | 1-10 | | | | min cannot | +| | | | | | | | | | | +| | list(string) | | | | | | | | | +| LStrIns | list(string) | ["c"] | | | | | ["c"] | false | | +| LStrInsNotList | list(string) | c | | | | | | | list of strings | +| 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"] | | | | | valid option | +| LStrInsNotOpts/NoDef | list(string) | ["c"] | | ["d"],["e"] | | | | | valid option | +| | | | | | | | | | | +| MulInsOpts | multi-select | ["c"] | ["e"] | c,d,e | | | ["c"] | true | | +| MulInsNotListOpts | multi-select | c | ["e"] | c,d,e | | | | | json encoded | +| MulInsNotOpts | multi-select | ["c"] | ["e"] | d,e | | | | | valid option | +| MulInsNotOpts/NoDef | multi-select | ["c"] | | d,e | | | | | valid option | +| MulInsInvOpts | multi-select | ["c"] | ["e"] | c,d,e | [a-c] | | | | regex cannot | \ No newline at end of file