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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy