-
Notifications
You must be signed in to change notification settings - Fork 18
Handle required fields #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6301a1c
1cd5246
3d5ca1f
e6aa841
a380a45
80d1cef
8c2e171
d04e513
097c99b
69cdd4f
866744a
4994a35
994e8fc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -211,6 +211,49 @@ func replaceEnvVars(f io.Reader, mapper func(string) string) (io.Reader, error) | |
| return strings.NewReader(os.Expand(buf.String(), mapper)), nil | ||
| } | ||
|
|
||
| func decodeAndCheckConfig(md toml.MetaData, compCfg interface{}) error { | ||
| var ( | ||
| cfg *toml.Primitive // config | ||
| dcfg interface{} // decoded config | ||
| name string // component name | ||
| typ string // component type | ||
| ) | ||
|
|
||
| switch t := compCfg.(type) { | ||
| case ConfigInput: | ||
| cfg, dcfg = t.Config, t.DecodedConfig | ||
| name, typ = t.Name, "input" | ||
| case ConfigFilter: | ||
| cfg, dcfg = t.Config, t.DecodedConfig | ||
| name, typ = t.Name, "filter" | ||
| case ConfigOutput: | ||
| cfg, dcfg = t.Config, t.DecodedConfig | ||
| name, typ = t.Name, "output" | ||
| case ConfigUpload: | ||
| cfg, dcfg = t.Config, t.DecodedConfig | ||
| name, typ = t.Name, "upload" | ||
| case ConfigMetrics: | ||
| cfg, dcfg = t.Config, t.DecodedConfig | ||
| name, typ = t.Name, "metrics" | ||
| default: | ||
| panic(fmt.Sprintf("unexpected type %#v", cfg)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. panic? shouldn't it return an error?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Getting an unexpected type here means there's a developer error which must be fixed, no way around that. |
||
| } | ||
|
|
||
| if req := CheckRequiredFields(dcfg); req != "" { | ||
| return fmt.Errorf("%s %q: %w", typ, name, ErrorRequiredField{req}) | ||
| } | ||
|
|
||
| if cfg == nil { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. uhm, does this ever happen? isn't already panicked in the switch?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| return nil | ||
| } | ||
|
|
||
| if err := md.PrimitiveDecode(*cfg, dcfg); err != nil { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. where did you see that? link?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the editor suggested that, but I now see it makes a mistake: the deprecated function is |
||
| return fmt.Errorf("%s %q: error parsing config: %v", typ, name, err) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // NewConfigFromToml creates a Config from a reader reading from a TOML | ||
| // configuration. comp describes all the existing components. | ||
| func NewConfigFromToml(f io.Reader, comp Components) (*Config, error) { | ||
|
|
@@ -287,44 +330,34 @@ func NewConfigFromToml(f io.Reader, comp Components) (*Config, error) { | |
|
|
||
| // Copy custom configuration structure, to prepare for re-reading | ||
| cfg.Input.DecodedConfig = cfg.Input.desc.Config | ||
| if cfg.Input.Config != nil { | ||
| if err := md.PrimitiveDecode(*cfg.Input.Config, cfg.Input.DecodedConfig); err != nil { | ||
| return nil, fmt.Errorf("error parsing input config: %v", err) | ||
| } | ||
| if err := decodeAndCheckConfig(md, cfg.Input); err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| for idx := range cfg.Filter { | ||
| // Clone the configuration object to allow the use of multiple instances of the same filter | ||
| cfg.Filter[idx].DecodedConfig = cloneConfig(cfg.Filter[idx].desc.Config) | ||
| if cfg.Filter[idx].Config != nil { | ||
| if err := md.PrimitiveDecode(*cfg.Filter[idx].Config, cfg.Filter[idx].DecodedConfig); err != nil { | ||
| return nil, fmt.Errorf("error parsing filter config: %v", err) | ||
| } | ||
| if err := decodeAndCheckConfig(md, cfg.Filter[idx]); err != nil { | ||
| return nil, err | ||
| } | ||
| } | ||
|
|
||
| cfg.Output.DecodedConfig = cfg.Output.desc.Config | ||
| if cfg.Output.Config != nil { | ||
| if err := md.PrimitiveDecode(*cfg.Output.Config, cfg.Output.DecodedConfig); err != nil { | ||
| return nil, fmt.Errorf("error parsing output config: %v", err) | ||
| } | ||
| if err := decodeAndCheckConfig(md, cfg.Output); err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| if cfg.Upload.Name != "" { | ||
| cfg.Upload.DecodedConfig = cfg.Upload.desc.Config | ||
| if cfg.Upload.Config != nil { | ||
| if err := md.PrimitiveDecode(*cfg.Upload.Config, cfg.Upload.DecodedConfig); err != nil { | ||
| return nil, fmt.Errorf("error parsing upload config: %v", err) | ||
| } | ||
| if err := decodeAndCheckConfig(md, cfg.Upload); err != nil { | ||
| return nil, err | ||
| } | ||
| } | ||
|
|
||
| if cfg.Metrics.Name != "" { | ||
| cfg.Metrics.DecodedConfig = cfg.Metrics.desc.Config | ||
| if cfg.Metrics.Config != nil { | ||
| if err := md.PrimitiveDecode(*cfg.Metrics.Config, cfg.Metrics.DecodedConfig); err != nil { | ||
| return nil, fmt.Errorf("error parsing metrics config: %v", err) | ||
| } | ||
| if err := decodeAndCheckConfig(md, cfg.Metrics); err != nil { | ||
| return nil, err | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -360,3 +393,63 @@ func NewConfigFromToml(f io.Reader, comp Components) (*Config, error) { | |
| // Fill-in with missing defaults | ||
| return &cfg, cfg.fillDefaults() | ||
| } | ||
|
|
||
| // hasConfig returns true if the underlying structure has at least one field. | ||
| func hasConfig(cfg interface{}) bool { | ||
| tf := reflect.TypeOf(cfg).Elem() | ||
| return tf.NumField() != 0 | ||
| } | ||
|
|
||
| // RequiredFields returns the names of the underlying configuration structure | ||
| // fields which are tagged as required. To tag a field as being required, a | ||
| // "required" struct struct tag must be present and set to true. | ||
| // | ||
| // RequiredFields doesn't support struct embedding other structs. | ||
| func RequiredFields(cfg interface{}) []string { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this public for testing purposes?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no. I thought this could be a good addition to the public API for users that desire to use baker as a library. |
||
| var fields []string | ||
|
|
||
| tf := reflect.TypeOf(cfg).Elem() | ||
| for i := 0; i < tf.NumField(); i++ { | ||
| field := tf.Field(i) | ||
|
|
||
| req := field.Tag.Get("required") | ||
| if req != "true" { | ||
| continue | ||
| } | ||
|
|
||
| fields = append(fields, field.Name) | ||
| } | ||
|
|
||
| return fields | ||
| } | ||
|
|
||
| // CheckRequiredFields checks that all fields that are tagged as required in | ||
| // cfg's type have actually been set to a value other than the field type zero | ||
| // value. If not CheckRequiredFields returns the name of the first required | ||
| // field that is not set, or, it returns an empty string if all required fields | ||
| // are set of the struct doesn't have any required fields (or any fields at all). | ||
| // | ||
| // CheckRequiredFields doesn't support struct embedding other structs. | ||
| func CheckRequiredFields(cfg interface{}) string { | ||
| fields := RequiredFields(cfg) | ||
|
|
||
| for _, name := range fields { | ||
| rv := reflect.ValueOf(cfg).Elem() | ||
| fv := rv.FieldByName(name) | ||
| if fv.IsZero() { | ||
| return name | ||
| } | ||
| } | ||
|
|
||
| return "" | ||
| } | ||
|
|
||
| // ErrorRequiredField describes the absence of a required field | ||
| // in a component configuration. | ||
| type ErrorRequiredField struct { | ||
| Field string // Field is the name of the missing field | ||
| } | ||
|
|
||
| func (e ErrorRequiredField) Error() string { | ||
| return fmt.Sprintf("%q is a required field", e.Field) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| package baker_test | ||
|
|
||
| import ( | ||
| "errors" | ||
| "reflect" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/AdRoll/baker" | ||
| "github.com/AdRoll/baker/filter/filtertest" | ||
| "github.com/AdRoll/baker/input" | ||
| ) | ||
|
|
||
| func TestRequiredFields(t *testing.T) { | ||
| type ( | ||
| test1 struct { | ||
| Name string | ||
| Value string `help:"field value" required:"false"` | ||
| } | ||
|
|
||
| test2 struct { | ||
| Name string | ||
| Value string `help:"field value" required:"true"` | ||
| } | ||
|
|
||
| test3 struct { | ||
| Name string `required:"true"` | ||
| Value string `help:"field value" required:"true"` | ||
| } | ||
| ) | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| cfg interface{} | ||
| want []string | ||
| }{ | ||
| { | ||
| name: "no required fields", | ||
| cfg: &test1{}, | ||
| want: nil, | ||
| }, | ||
| { | ||
| name: "one required field", | ||
| cfg: &test2{}, | ||
| want: []string{"Value"}, | ||
| }, | ||
| { | ||
| name: "all required fields", | ||
| cfg: &test3{}, | ||
| want: []string{"Name", "Value"}, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| if got := baker.RequiredFields(tt.cfg); !reflect.DeepEqual(got, tt.want) { | ||
| t.Errorf("RequiredFields() = %v, want %v", got, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestCheckRequiredFields(t *testing.T) { | ||
| type ( | ||
| test1 struct { | ||
| Name string | ||
| Value string `help:"field value" required:"false"` | ||
| } | ||
|
|
||
| test2 struct { | ||
| Name string | ||
| Value string `help:"field value" required:"true"` | ||
| } | ||
|
|
||
| test3 struct { | ||
| Name string `required:"true"` | ||
| Value string `help:"field value" required:"true"` | ||
| } | ||
| ) | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| val interface{} | ||
| want string | ||
| }{ | ||
| { | ||
| name: "no required fields", | ||
| val: &test1{}, | ||
| want: "", | ||
| }, | ||
| { | ||
| name: "one missing required field ", | ||
| val: &test2{Name: "name", Value: ""}, | ||
| want: "Value", | ||
| }, | ||
| { | ||
| name: "one present required field ", | ||
| val: &test2{Name: "name", Value: "value"}, | ||
| want: "", | ||
| }, | ||
| { | ||
| name: "all required fields and all are missing", | ||
| val: &test3{}, | ||
| want: "Name", | ||
| }, | ||
| { | ||
| name: "all required fields but the first missing", | ||
| val: &test3{Value: "value"}, | ||
| want: "Name", | ||
| }, | ||
| { | ||
| name: "all required fields and all are present", | ||
| val: &test3{Name: "name", Value: "value"}, | ||
| want: "", | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| if got := baker.CheckRequiredFields(tt.val); got != tt.want { | ||
| t.Errorf("CheckRequiredFields() = %v, want %v", got, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestNewConfigFromTOMLRequiredField(t *testing.T) { | ||
| type dummyConfig struct { | ||
| Param1 string | ||
| Param2 string `required:"true"` | ||
| } | ||
| var dummyDesc = baker.OutputDesc{ | ||
| Name: "Dummy", | ||
| New: func(baker.OutputParams) (baker.Output, error) { return nil, nil }, | ||
| Config: &dummyConfig{}, | ||
| } | ||
|
|
||
| toml := ` | ||
| [input] | ||
| name = "List" | ||
|
|
||
| [input.config] | ||
| files=["testdata/input.csv.zst"] | ||
|
|
||
| [output] | ||
| name = "Dummy" | ||
| procs=1 | ||
| [output.config] | ||
| param1="this parameter is set" | ||
| #param2="this parameter is not set" | ||
| ` | ||
|
|
||
| components := baker.Components{ | ||
| Inputs: []baker.InputDesc{input.ListDesc}, | ||
| Filters: []baker.FilterDesc{filtertest.PassThroughDesc}, | ||
| Outputs: []baker.OutputDesc{dummyDesc}, | ||
| } | ||
|
|
||
| _, err := baker.NewConfigFromToml(strings.NewReader(toml), components) | ||
| if err == nil { | ||
| t.Fatal("expected an error") | ||
| } | ||
|
|
||
| var errReq baker.ErrorRequiredField | ||
| if !errors.As(err, &errReq) { | ||
| t.Fatalf("got %q, want a ErrorRequiredField", err) | ||
| } | ||
|
|
||
| if errReq.Field != "Param2" { | ||
| t.Errorf("got field=%q, want field=%q", errReq.Field, "Param2") | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
already included in the file