|
| 1 | +// Package envvar provides declarative, non-fatal parsing of configuration from |
| 2 | +// environment variables on top of kelseyhightower/envconfig. |
| 3 | +// |
| 4 | +// The decoder types here implement envconfig.Decoder so that configuration is |
| 5 | +// parsed via struct tags while preserving the lenient, non-fatal parsing |
| 6 | +// behaviour the service relied on before adopting envconfig: an unparseable |
| 7 | +// value logs a warning and falls back to the zero value rather than failing |
| 8 | +// startup. Callers apply their own business defaults to the zero value via the |
| 9 | +// historical "<= 0" / empty-string guards. |
| 10 | +// |
| 11 | +// It lives in its own package so the central config package and the per-engine |
| 12 | +// packages (mosip, sunbird) can all share the same parsing helpers. |
| 13 | +package envvar |
| 14 | + |
| 15 | +import ( |
| 16 | + "reflect" |
| 17 | + "strconv" |
| 18 | + "time" |
| 19 | + |
| 20 | + "github.com/kelseyhightower/envconfig" |
| 21 | + |
| 22 | + applog "github.com/mosip/esignet/internal/log" |
| 23 | +) |
| 24 | + |
| 25 | +// LaxBool is a bool that accepts the historical set of spellings |
| 26 | +// (1/true/TRUE/yes/YES → true; 0/false/FALSE/no/NO → false) and warns + |
| 27 | +// defaults to false on anything else. It mirrors the former envBool helper. |
| 28 | +type LaxBool bool |
| 29 | + |
| 30 | +func (b *LaxBool) Decode(value string) error { |
| 31 | + switch value { |
| 32 | + case "", "0", "false", "FALSE", "no", "NO": |
| 33 | + *b = false |
| 34 | + case "1", "true", "TRUE", "yes", "YES": |
| 35 | + *b = true |
| 36 | + default: |
| 37 | + applog.GetLogger().Warn( |
| 38 | + "invalid boolean environment variable value", |
| 39 | + applog.String("value", value), |
| 40 | + ) |
| 41 | + *b = false |
| 42 | + } |
| 43 | + return nil |
| 44 | +} |
| 45 | + |
| 46 | +// LaxInt is an int parsed with strconv.Atoi that warns + falls back to 0 on an |
| 47 | +// invalid value instead of failing. It mirrors the former envInt helper. |
| 48 | +type LaxInt int |
| 49 | + |
| 50 | +func (i *LaxInt) Decode(value string) error { |
| 51 | + if value == "" { |
| 52 | + *i = 0 |
| 53 | + return nil |
| 54 | + } |
| 55 | + parsed, err := strconv.Atoi(value) |
| 56 | + if err != nil { |
| 57 | + applog.GetLogger().Warn( |
| 58 | + "invalid integer environment variable value", |
| 59 | + applog.String("value", value), |
| 60 | + ) |
| 61 | + *i = 0 |
| 62 | + return nil |
| 63 | + } |
| 64 | + *i = LaxInt(parsed) |
| 65 | + return nil |
| 66 | +} |
| 67 | + |
| 68 | +// LaxInt64 is an int64 parsed with strconv.ParseInt that warns + falls back to |
| 69 | +// 0 on an invalid value. It mirrors the former envInt64 helper. |
| 70 | +type LaxInt64 int64 |
| 71 | + |
| 72 | +func (i *LaxInt64) Decode(value string) error { |
| 73 | + if value == "" { |
| 74 | + *i = 0 |
| 75 | + return nil |
| 76 | + } |
| 77 | + parsed, err := strconv.ParseInt(value, 10, 64) |
| 78 | + if err != nil { |
| 79 | + applog.GetLogger().Warn( |
| 80 | + "invalid integer environment variable value", |
| 81 | + applog.String("value", value), |
| 82 | + ) |
| 83 | + *i = 0 |
| 84 | + return nil |
| 85 | + } |
| 86 | + *i = LaxInt64(parsed) |
| 87 | + return nil |
| 88 | +} |
| 89 | + |
| 90 | +// SecondsDuration is a time.Duration parsed from a plain integer number of |
| 91 | +// seconds — the historical *_SECS convention — rather than Go duration syntax. |
| 92 | +// Invalid values warn + fall back to 0; callers supply their own default when |
| 93 | +// 0 is not a meaningful value. |
| 94 | +type SecondsDuration time.Duration |
| 95 | + |
| 96 | +func (d *SecondsDuration) Decode(value string) error { |
| 97 | + if value == "" { |
| 98 | + *d = 0 |
| 99 | + return nil |
| 100 | + } |
| 101 | + secs, err := strconv.Atoi(value) |
| 102 | + if err != nil { |
| 103 | + applog.GetLogger().Warn( |
| 104 | + "invalid integer environment variable value", |
| 105 | + applog.String("value", value), |
| 106 | + ) |
| 107 | + *d = 0 |
| 108 | + return nil |
| 109 | + } |
| 110 | + *d = SecondsDuration(time.Duration(secs) * time.Second) |
| 111 | + return nil |
| 112 | +} |
| 113 | + |
| 114 | +// Process populates spec from environment variables. Parsing of individual |
| 115 | +// fields never fails startup (see the Lax* decoders); a non-nil error here is |
| 116 | +// therefore unexpected and only logged. |
| 117 | +// |
| 118 | +// It also restores the historical "an empty value means use the default" |
| 119 | +// semantics: envconfig only applies a `default` tag when the variable is |
| 120 | +// entirely unset, whereas the previous envOrDefault helper also fell back to |
| 121 | +// the default when the variable was set to an empty string. applyStringDefaults |
| 122 | +// reinstates that behaviour for string fields. |
| 123 | +func Process(spec any) { |
| 124 | + if err := envconfig.Process("", spec); err != nil { |
| 125 | + applog.GetLogger().Warn("process configuration", applog.Error(err)) |
| 126 | + } |
| 127 | + applyStringDefaults(spec) |
| 128 | +} |
| 129 | + |
| 130 | +// applyStringDefaults fills any empty string field that carries a non-empty |
| 131 | +// `default` tag with that default. |
| 132 | +// |
| 133 | +// NOTE: this only walks top-level string fields. Every spec that uses this is a |
| 134 | +// flat struct, which keeps the walk simple; if a spec ever nests a sub-struct |
| 135 | +// with a defaulted string field, this must be made recursive or that field will |
| 136 | +// silently miss the empty-means-default behaviour. |
| 137 | +func applyStringDefaults(spec any) { |
| 138 | + v := reflect.ValueOf(spec).Elem() |
| 139 | + t := v.Type() |
| 140 | + for i := 0; i < t.NumField(); i++ { |
| 141 | + field := v.Field(i) |
| 142 | + if field.Kind() != reflect.String || !field.CanSet() { |
| 143 | + continue |
| 144 | + } |
| 145 | + def := t.Field(i).Tag.Get("default") |
| 146 | + if def != "" && field.String() == "" { |
| 147 | + field.SetString(def) |
| 148 | + } |
| 149 | + } |
| 150 | +} |
0 commit comments