package confiq import ( "encoding/json" "errors" "fmt" "net" "net/url" "reflect" "time" ) var ( errIPCannotBeNil = errors.New("IP address cannot be nil") errCannotParseIP = errors.New("cannot parse IP address") errURLCannotBeNil = errors.New("URL cannot be nil") errCannotParseURL = errors.New("cannot parse URL") errDurationCannotBeNil = errors.New("duration cannot be nil") errCannotParseDuration = errors.New("cannot parse duration") errTimeCannotBeNil = errors.New("time cannot be nil") errCannotParseTime = errors.New("cannot parse time") errCannotParseNonStringTime = errors.New("cannot parse time from non-string type") errJSONRawMessageCannotBeNil = errors.New("JSON raw message cannot be nil") errCannotParseJSONRawMessage = errors.New("cannot marshal source value to JSON") ) type typeDefinition struct { kind reflect.Kind pkgPath string typeName string } var commonDecoders = map[typeDefinition]decoderFunc{ {reflect.Int64, "time", "Duration"}: decodeDuration, {reflect.Slice, "net", "IP"}: decodeIP, {reflect.Slice, "encoding/json", "RawMessage"}: decodeJSONRawMessage, {reflect.Struct, "time", "Time"}: decodeTime, {reflect.Struct, "net/url", "URL"}: decodeURL, } func getCommonDecoder(targetValType reflect.Type) decoderFunc { if targetValType.Kind() == reflect.Ptr { targetValType = targetValType.Elem() } if decoder, decoderFound := commonDecoders[typeDefinition{targetValType.Kind(), targetValType.PkgPath(), targetValType.Name()}]; decoderFound { return decoder } return nil } func decodeDuration(targetValue reflect.Value, sourceValue any) error { if sourceValue == nil { return errDurationCannotBeNil } parsedDuration, err := time.ParseDuration(castToString(sourceValue)) if err != nil { return fmt.Errorf("%w: %w", errCannotParseDuration, err) } targetValue.SetInt(int64(parsedDuration)) return nil } func decodeIP(targetValue reflect.Value, sourceValue any) error { if sourceValue == nil { return errIPCannotBeNil } parsedIP := net.ParseIP(castToString(sourceValue)) if parsedIP == nil { return errCannotParseIP } targetValue.Set(reflect.ValueOf(parsedIP)) return nil } func decodeJSONRawMessage(targetValue reflect.Value, sourceValue any) error { if sourceValue == nil { return errJSONRawMessageCannotBeNil } marshaledSourceValue, err := json.Marshal(sourceValue) if err != nil { return fmt.Errorf("%w: %w", errCannotParseJSONRawMessage, err) } targetValue.Set(reflect.ValueOf(marshaledSourceValue)) return nil } func decodeTime(targetValue reflect.Value, sourceValue any) error { if sourceValue == nil { return errTimeCannotBeNil } if timeValue, isTimeValue := sourceValue.(time.Time); isTimeValue { targetValue.Set(reflect.ValueOf(timeValue)) return nil } stringValue, isStringValue := sourceValue.(string) if !isStringValue { return fmt.Errorf("%w: %T", errCannotParseNonStringTime, sourceValue) } parsedTime, err := time.Parse(time.RFC3339, stringValue) if err != nil { return fmt.Errorf("%w: %w", errCannotParseTime, err) } targetValue.Set(reflect.ValueOf(parsedTime)) return nil } func decodeURL(targetValue reflect.Value, sourceValue any) error { if sourceValue == nil { return errURLCannotBeNil } parsedURL, err := url.Parse(castToString(sourceValue)) if err != nil { return fmt.Errorf("%w: %w", errCannotParseURL, err) } targetValue.Set(reflect.ValueOf(parsedURL)) return nil }
// Package confiq is a Go package for populating structs from structured data formats such JSON, TOML, YAML or Env. package confiq import ( "errors" "fmt" "reflect" ) const defaultTag = "cfg" var ( errCannotGetKeyFromNonMap = errors.New("cannot get key from non-map type") errKeyNotFound = errors.New("key not found") errCannotGetIndexFromNonSlice = errors.New("cannot get index from non-slice type") errIndexOutOfBounds = errors.New("index out of bounds") ) type decoder struct { tag string } type decodeSettings struct { strict bool prefix string } // ConfigSet is a configuration set that can be used to load and decode configuration values into a struct. type ConfigSet struct { value *any decoder *decoder } // New creates a new ConfigSet with the given options. func New(options ...configSetOption) *ConfigSet { var ( value any configSet = &ConfigSet{ value: &value, decoder: &decoder{tag: defaultTag}, } ) for _, option := range options { option(configSet) } return configSet } // Get returns the configuration value at the given path as an interface. func (c *ConfigSet) Get(path string) (any, error) { return c.getByPath(path) } func (c *ConfigSet) getByPath(path string) (any, error) { currentValue := *c.value for path != "" { var currentSegment segment currentSegment, path = getNextSegment(path) switch v := currentSegment.(type) { case keySegment: segmentValue, err := getMapValue(currentValue, v.asString()) if err != nil { return nil, err } currentValue = segmentValue case indexSegment: segmentValue, err := getSliceValue(currentValue, v.asInt()) if err != nil { return nil, err } currentValue = segmentValue } } return currentValue, nil } func getMapValue(originMap any, key string) (any, error) { v := reflect.ValueOf(originMap) if v.Kind() != reflect.Map { return nil, fmt.Errorf("%w: %s(%T)", errCannotGetKeyFromNonMap, key, originMap) } value := v.MapIndex(reflect.ValueOf(key)) if !value.IsValid() { return nil, fmt.Errorf("%w: %s", errKeyNotFound, key) } return value.Interface(), nil } func getSliceValue(originSlice any, index int) (any, error) { v := reflect.ValueOf(originSlice) if v.Kind() != reflect.Slice { return nil, fmt.Errorf("%w: %d(%T)", errCannotGetIndexFromNonSlice, index, originSlice) } if v.Len() <= index || index < 0 { return nil, fmt.Errorf("%w: %d", errIndexOutOfBounds, index) } return v.Index(index).Interface(), nil }
package confiq import ( "encoding" "errors" "fmt" "reflect" "strings" ) const sliceSplitChar = ";" // Collection of decode errors. var ( ErrInvalidTarget = errors.New("target must be non-nil pointer to a slice, map or struct that has at least one exported field with a the configured tag") ErrNoTargetFieldsAreSet = errors.New("none of the target fields were set from config values") ) var ( errCannotDecodeCustomTypeField = errors.New("cannot decode field with custom decoder") errCannotDecodeNonRequiredField = errors.New("cannot decode non-strict field") errCannotDecodeNonSliceValueToTarget = errors.New("cannot decode non-slice value to target") errCannotUnmarshalPrimitive = errors.New("cannot unmarshal primitive as text") errCannotHaveDefaultForRequiredField = errors.New("cannot have default value for required field") errUnsupportedPrimitiveKind = errors.New("unsupported primitive kind") ) type ( decoderFunc func(targetField reflect.Value, value any) error fieldDecoderFunc func(targetField reflect.Value, value any, strict bool) (int, error) ) type fieldOptions struct { path string strict bool required bool defaultValue *string } type Decoder interface { Decode(value any) error } // Decode decodes the configuration values into the target struct. func (c *ConfigSet) Decode(target interface{}, options ...decodeOption) error { return c.decode(target, options) } func (c *ConfigSet) decode(target interface{}, options []decodeOption) error { decodeSettings := &decodeSettings{ strict: false, prefix: "", } for _, option := range options { option(decodeSettings) } targetValue := reflect.ValueOf(target) if targetValue.Kind() != reflect.Ptr || targetValue.IsNil() { return ErrInvalidTarget } targetValue = targetValue.Elem() decodedFieldCount, err := c.decodeField(targetValue, fieldOptions{ path: decodeSettings.prefix, strict: decodeSettings.strict, required: false, defaultValue: nil, }) if err != nil { return err } else if decodedFieldCount == 0 { return ErrNoTargetFieldsAreSet } return nil } func (c *ConfigSet) getFieldConfigValue(fieldOpts fieldOptions) (any, error) { if fieldOpts.required && fieldOpts.defaultValue != nil { return nil, fmt.Errorf("%w: %s", errCannotHaveDefaultForRequiredField, fieldOpts.path) } configValue, err := c.getByPath(fieldOpts.path) if err != nil { if fieldOpts.required { return nil, fmt.Errorf("field is required: %w", err) } if fieldOpts.defaultValue != nil { return *fieldOpts.defaultValue, nil } return nil, errCannotDecodeNonRequiredField } return configValue, nil } func (c *ConfigSet) decodeField(targetValue reflect.Value, fieldOpts fieldOptions) (int, error) { fieldConfigValue, err := c.getFieldConfigValue(fieldOpts) if err != nil { if !errors.Is(err, errCannotDecodeNonRequiredField) { return 0, err } } var ( decodedFields int decodeErr error fieldDecoder fieldDecoderFunc ) if commonDecoder := getCommonDecoder(targetValue.Type()); commonDecoder != nil { if err := commonDecoder(targetValue, fieldConfigValue); err != nil { if fieldOpts.strict { return 0, fmt.Errorf("error decoding field value: %w", err) } return 0, nil } return 1, nil } if targetValue.Kind() == reflect.Ptr { dereferencedTargetValue := reflect.New(targetValue.Type().Elem()).Elem() decodedFields, err := c.decodeField(dereferencedTargetValue, fieldOpts) if err != nil { if fieldOpts.strict { return 0, fmt.Errorf("error decoding pointer value: %w", err) } return 0, nil } targetValue.Set(dereferencedTargetValue.Addr()) return decodedFields, nil } // check if targetValue implements Decoder interface if decoder, ok := targetValue.Addr().Interface().(Decoder); ok { if err := decoder.Decode(fieldConfigValue); err != nil { return 0, fmt.Errorf("%w: %w", errCannotDecodeCustomTypeField, err) } return 1, nil } switch targetValue.Kind() { case reflect.Map: fieldDecoder = c.decodeMap case reflect.Slice: fieldDecoder = c.decodeSlice case reflect.Struct: fieldDecoder = c.decodeStruct default: fieldDecoder = c.decodePrimitiveType } decodedFields, decodeErr = fieldDecoder(targetValue, fieldConfigValue, fieldOpts.strict) if decodeErr != nil { return 0, decodeErr } return decodedFields, nil } func (c *ConfigSet) decodeMap(targetMapValue reflect.Value, configValue any, strict bool) (int, error) { var ( configMapValue = reflect.ValueOf(configValue) targetMapValueType = targetMapValue.Type() targetKeyType = targetMapValueType.Key() targetValueType = targetMapValueType.Elem() setFieldCount = 0 ) // setup empty map targetMapValue.Set(reflect.MakeMap(targetMapValueType)) for _, key := range configMapValue.MapKeys() { var ( k = reflect.New(targetKeyType).Elem() v = reflect.New(targetValueType).Elem() ) // decode map key _, err := c.decodePrimitiveType(k, key.Interface(), strict) if err != nil { return 0, fmt.Errorf("error decoding map key: %w", err) } // decode map value decodedFieldCount, err := c.subValue(configValue). decodeField(v, fieldOptions{ path: keySegment(key.String()).String(), strict: strict, required: false, defaultValue: nil, }) if err != nil { return 0, fmt.Errorf("error decoding map value: %w", err) } targetMapValue.SetMapIndex(k, v) setFieldCount += decodedFieldCount } return setFieldCount, nil } func (c *ConfigSet) decodeStruct(targetStructValue reflect.Value, configValue any, strict bool) (int, error) { var ( targetStructType = targetStructValue.Type() setFieldCount = 0 ) for i := range targetStructValue.NumField() { // get the struct field's tag and options targetStructFieldOpts := c.readTag(targetStructType.Field(i), c.decoder.tag) // get the struct field's reflection value targetStructFieldValue := targetStructValue.Field(i) // check if the field is exported if !targetStructFieldValue.CanSet() || !targetStructFieldValue.Addr().CanInterface() { continue } // set the field's strictness targetStructFieldOpts.strict = strict || targetStructFieldOpts.strict // decode the field decodedFieldCount, err := c.subValue(configValue). decodeField(targetStructFieldValue, targetStructFieldOpts) if err != nil { return 0, fmt.Errorf("error decoding struct field value: %w", err) } setFieldCount += decodedFieldCount } return setFieldCount, nil } func (c *ConfigSet) decodeSlice(targetSliceValue reflect.Value, configValue any, strict bool) (int, error) { var ( configSliceValue = reflect.ValueOf(configValue) configSliceValueKind = configSliceValue.Kind() setFieldCount = 0 ) if configSliceValueKind != reflect.Slice { if configSliceValueKind != reflect.String { return 0, fmt.Errorf("%w: %v", errCannotDecodeNonSliceValueToTarget, configSliceValueKind) } return c.decodeSlice(targetSliceValue, strings.Split(configSliceValue.String(), sliceSplitChar), strict) } configSliceValueLength := configSliceValue.Len() // Set slice to the same length as the config slice targetSliceValue.Set(reflect.MakeSlice(targetSliceValue.Type(), configSliceValueLength, configSliceValueLength)) // Decode each element based on its type for i := range configSliceValueLength { decodedFieldCount, err := c.subValue(configValue). decodeField(targetSliceValue.Index(i), fieldOptions{ path: indexSegment(i).String(), strict: strict, required: false, defaultValue: nil, }) if err != nil { return setFieldCount, fmt.Errorf("error decoding slice element value: %w", err) } setFieldCount += decodedFieldCount } return setFieldCount, nil } func (c *ConfigSet) decodePrimitiveType(primitiveValue reflect.Value, configValue any, strict bool) (int, error) { primitiveInterface := primitiveValue.Addr().Interface() // check if primitive implements encoding.TextUnmarshaler interface if unmarshaler, ok := primitiveInterface.(encoding.TextUnmarshaler); ok { if err := unmarshaler.UnmarshalText(castToBytes(configValue)); err != nil { return 0, fmt.Errorf("%w: %w", errCannotUnmarshalPrimitive, err) } primitiveValue.Set(reflect.ValueOf(primitiveInterface).Elem()) return 1, nil } // select the appropriate decoder function based on the primitive's kind var ( primitiveDecoderFunc decoderFunc primitiveValueKind = primitiveValue.Kind() ) switch primitiveValueKind { case reflect.Bool: primitiveDecoderFunc = decodeBool case reflect.String: primitiveDecoderFunc = decodeString case reflect.Float32, reflect.Float64: primitiveDecoderFunc = decodeFloat case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: primitiveDecoderFunc = decodeInt case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: primitiveDecoderFunc = decodeUint default: return 0, fmt.Errorf("%w: %v", errUnsupportedPrimitiveKind, primitiveValueKind) } if err := primitiveDecoderFunc(primitiveValue, configValue); err != nil { if strict { return 0, fmt.Errorf("error decoding primitive value: %w", err) } return 0, nil } return 1, nil } func (c *ConfigSet) readTag(field reflect.StructField, tag string) fieldOptions { fieldOpts := fieldOptions{ path: "", strict: false, required: false, defaultValue: nil, } tagValue := field.Tag.Get(tag) if tagValue == "" { return fieldOpts } tagParts := strings.Split(tagValue, ",") fieldOpts.path = tagParts[0] // read the remaining tag parts for _, part := range tagParts[1:] { if part == "strict" { fieldOpts.strict = true continue } if part == "required" { fieldOpts.required = true continue } if strings.HasPrefix(part, "default=") { devaultValue := part[8:] fieldOpts.defaultValue = &devaultValue continue } } return fieldOpts } func (c *ConfigSet) subValue(value any) *ConfigSet { return &ConfigSet{ value: &value, decoder: c.decoder, } } func castToBytes(value any) []byte { return []byte(castToString(value)) } func castToString(value any) string { if stringValue, ok := value.(string); ok { return stringValue } return fmt.Sprintf("%v", value) }
package confiq import ( "errors" "fmt" "maps" ) const noPrefix = "" // Collection of loader errors. var ( ErrCannotOpenConfig = errors.New("cannot open config") ErrCannotLoadConfig = errors.New("cannot load config") ) var ( errValueCannotBeNil = errors.New("value cannot be nil") errCannotApplyValueOfThisType = errors.New("cannot apply value of this type") errCannotApplyMapValue = errors.New("cannot apply map value") errCannotApplySliceValue = errors.New("cannot apply slice value") ) type loader struct { prefix string } func (c *ConfigSet) Load(valueContainer IValueContainer, options ...loadOption) error { if errs := valueContainer.Errors(); len(errs) > 0 { return fmt.Errorf("%w: %w", ErrCannotLoadConfig, errors.Join(errs...)) } return c.applyValues(valueContainer.Get(), options...) } // LoadRawValue loads a raw value into the config set. // The value must be a map[string]any or a slice of any. func (c *ConfigSet) LoadRawValue(newValues []any, options ...loadOption) error { if newValues == nil { return errValueCannotBeNil } return c.applyValues(newValues, options...) } func newLoader() *loader { return &loader{ prefix: noPrefix, } } func (c *ConfigSet) applyValues(newValues []any, options ...loadOption) error { loader := newLoader() for _, option := range options { option(loader) } for _, newValue := range newValues { switch v := newValue.(type) { case map[string]any: if err := c.applyMap(v, loader.prefix); err != nil { return err } case []any: if err := c.applySlice(v, loader.prefix); err != nil { return err } default: return fmt.Errorf("%w: %T", errCannotApplyValueOfThisType, newValue) } } return nil } func (c *ConfigSet) applyMap(newValue map[string]any, prefix string) error { if prefix == "" { return c.applyMapWithoutPrefix(newValue) } return c.applyMapWithPrefix(newValue, prefix) } func (c *ConfigSet) applySlice(newValue []any, prefix string) error { if prefix == "" { return c.applySliceWithoutPrefix(newValue) } return c.applySliceWithPrefix(newValue, prefix) } func (c *ConfigSet) applyMapWithoutPrefix(newValue map[string]any) error { if *c.value == nil { *c.value = newValue return nil } valueMap, ok := (*c.value).(map[string]any) if !ok { return errCannotApplyMapValue } maps.Copy(valueMap, newValue) return nil } func (c *ConfigSet) applyMapWithPrefix(newValue map[string]any, prefix string) error { if *c.value == nil { *c.value = map[string]any{} } valueMap, ok := (*c.value).(map[string]any) if !ok { return errCannotApplyMapValue } valueMapAtPath, ok := valueMap[prefix] if !ok { valueMap[prefix] = newValue return nil } valueMapAtPathMap, ok := valueMapAtPath.(map[string]any) if !ok { return errCannotApplyMapValue } maps.Copy(valueMapAtPathMap, newValue) return nil } func (c *ConfigSet) applySliceWithoutPrefix(newValue []any) error { if *c.value == nil { *c.value = newValue return nil } valueSlice, ok := (*c.value).([]any) if !ok { return errCannotApplySliceValue } valueSlice = append(valueSlice, newValue...) *c.value = valueSlice return nil } func (c *ConfigSet) applySliceWithPrefix(newValue []any, prefix string) error { if *c.value == nil { *c.value = map[string]any{} } valueMap, ok := (*c.value).(map[string]any) if !ok { return errCannotApplySliceValue } valueMapAtPath, ok := valueMap[prefix] if !ok { valueMap[prefix] = newValue return nil } valueMapAtPathSlice, ok := valueMapAtPath.([]any) if !ok { return errCannotApplySliceValue } valueMapAtPathSlice = append(valueMapAtPathSlice, newValue...) valueMap[prefix] = valueMapAtPathSlice return nil }
// Package confiqenv allows confiq values to be loaded from Env format. package confiqenv import ( "bytes" "errors" "fmt" "io" "os" "path/filepath" "strings" "github.com/hashicorp/go-envparse" ) const ( envSplitChar = "=" envSplitElements = 2 ) var ( ErrCannotGetBytesFromReader = errors.New("cannot get bytes from reader") ErrCannotOpenEnvFile = errors.New("cannot open Env file") ErrCannotReadEnvData = errors.New("cannot read Env data") ) // Container is a struct that holds the loaded values. type Container struct { values []any errors []error } // Get returns the loaded JSON values. func (c *Container) Get() []any { return c.values } // Errors returns the errors that occurred during the loading process. func (c *Container) Errors() []error { return c.errors } // Load creates an empty container, into which the JSON values can be loaded. func Load() *Container { container := new(Container) return container } // FromEnvironment loads config data from the environment variables. func (c *Container) FromEnvironment() *Container { var ( envSlice = os.Environ() envMap = make(map[string]any) ) for _, envElement := range envSlice { envKeyValue := strings.SplitN(envElement, envSplitChar, envSplitElements) envMap[envKeyValue[0]] = envKeyValue[1] } c.values = append(c.values, envMap) return nil } // FromFile loads a Env file from the given path. func (c *Container) FromFile(path string) *Container { inputBytes, err := os.ReadFile(filepath.Clean(path)) if err != nil { c.errors = append(c.errors, fmt.Errorf("%w: %w", ErrCannotOpenEnvFile, err)) return c } return c.FromBytes(inputBytes) } // FromString loads a Env file from the given string. func (c *Container) FromString(input string) *Container { return c.FromBytes([]byte(input)) } // FromBytes loads a Env file from the given bytes. func (c *Container) FromBytes(input []byte) *Container { return c.FromReader(bytes.NewReader(input)) } // FromReader loads a Env file from a reader stream. func (c *Container) FromReader(reader io.Reader) *Container { if reader == nil { c.errors = append(c.errors, ErrCannotReadEnvData) return c } parsedEnvMap, err := envparse.Parse(reader) if err != nil { c.errors = append(c.errors, fmt.Errorf("%w: %w", ErrCannotReadEnvData, err)) return c } c.values = append(c.values, envMapAsAny(parsedEnvMap)) return c } func envMapAsAny(envMap map[string]string) map[string]any { anyMap := make(map[string]any) for key, value := range envMap { anyMap[key] = value } return anyMap }
// Package confiqjson allows confiq values to be loaded from JSON format. package confiqjson import ( "bytes" "encoding/json" "errors" "fmt" "io" "os" "path/filepath" ) var ( ErrCannotOpenJSONFile = errors.New("cannot open JSON file") ErrCannotReadJSONData = errors.New("cannot read JSON data") ErrCannotReadJSONBytes = errors.New("cannot read JSON bytes") ) // Container is a struct that holds the loaded values. type Container struct { values []any errors []error } // Get returns the loaded JSON values. func (c *Container) Get() []any { return c.values } // Errors returns the errors that occurred during the loading process. func (c *Container) Errors() []error { return c.errors } // Load creates an empty container, into which the JSON values can be loaded. func Load() *Container { container := new(Container) return container } // FromFile loads a JSON file from the given path. func (c *Container) FromFile(path string) *Container { bytes, err := os.ReadFile(filepath.Clean(path)) if err != nil { c.errors = append(c.errors, fmt.Errorf("%w: %w", ErrCannotOpenJSONFile, err)) return c } c.readFromBytes(bytes) return c } // FromString loads a JSON file from the given string. func (c *Container) FromString(input string) *Container { c.readFromBytes([]byte(input)) return c } // FromReader loads a JSON file from a reader stream. func (c *Container) FromReader(reader io.Reader) *Container { if reader == nil { c.errors = append(c.errors, ErrCannotReadJSONData) return c } buffer := new(bytes.Buffer) if _, err := buffer.ReadFrom(reader); err != nil { c.errors = append(c.errors, fmt.Errorf("%w: %w", ErrCannotReadJSONData, err)) return c } c.readFromBytes(buffer.Bytes()) return c } // FromBytes loads a JSON file from the given bytes. func (c *Container) FromBytes(input []byte) *Container { c.readFromBytes(input) return c } func (c *Container) readFromBytes(input []byte) { var value any if err := json.Unmarshal(input, &value); err != nil { c.errors = append(c.errors, fmt.Errorf("%w: %w", ErrCannotReadJSONBytes, err)) return } c.values = append(c.values, value) }
// Package confiqtoml allows confiq values to be loaded from TOML format. package confiqtoml import ( "bytes" "errors" "fmt" "io" "os" "path/filepath" "github.com/pelletier/go-toml" ) var ( ErrCannotOpenTOMLFile = errors.New("cannot open TOML file") ErrCannotReadTOMLData = errors.New("cannot read TOML data") ErrCannotReadTOMLBytes = errors.New("cannot read TOML bytes") ) // Container is a struct that holds the loaded values. type Container struct { values []any errors []error } // Get returns the loaded TOML values. func (c *Container) Get() []any { return c.values } // Errors returns the errors that occurred during the loading process. func (c *Container) Errors() []error { return c.errors } // Load creates an empty container, into which the TOML values can be loaded. func Load() *Container { container := new(Container) return container } // FromFile loads a TOML file from the given path. func (c *Container) FromFile(path string) *Container { bytes, err := os.ReadFile(filepath.Clean(path)) if err != nil { c.errors = append(c.errors, fmt.Errorf("%w: %w", ErrCannotOpenTOMLFile, err)) return c } c.readFromBytes(bytes) return c } // FromString loads a TOML file from the given string. func (c *Container) FromString(input string) *Container { c.readFromBytes([]byte(input)) return c } // FromReader loads a TOML file from a reader stream. func (c *Container) FromReader(reader io.Reader) *Container { if reader == nil { c.errors = append(c.errors, ErrCannotReadTOMLData) return c } buffer := new(bytes.Buffer) if _, err := buffer.ReadFrom(reader); err != nil { c.errors = append(c.errors, fmt.Errorf("%w: %w", ErrCannotReadTOMLData, err)) return c } c.readFromBytes(buffer.Bytes()) return c } // FromBytes loads a TOML file from the given bytes. func (c *Container) FromBytes(input []byte) *Container { c.readFromBytes(input) return c } func (c *Container) readFromBytes(input []byte) { var value any if err := toml.Unmarshal(input, &value); err != nil { c.errors = append(c.errors, fmt.Errorf("%w: %w", ErrCannotReadTOMLBytes, err)) return } c.values = append(c.values, value) }
// Package confiqyaml allows confiq values to be loaded from YAML format. package confiqyaml import ( "bytes" "errors" "fmt" "io" "os" "path/filepath" "gopkg.in/yaml.v3" ) var ( ErrCannotOpenYAMLFile = errors.New("cannot open YAML file") ErrCannotReadYAMLData = errors.New("cannot read YAML data") ErrCannotReadYAMLBytes = errors.New("cannot read YAML bytes") ) // Container is a struct that holds the loaded values. type Container struct { values []any errors []error } // Get returns the loaded YAML values. func (c *Container) Get() []any { return c.values } // Errors returns the errors that occurred during the loading process. func (c *Container) Errors() []error { return c.errors } // Load creates an empty container, into which the YAML values can be loaded. func Load() *Container { container := new(Container) return container } // FromFile loads a YAML file from the given path. func (c *Container) FromFile(path string) *Container { bytes, err := os.ReadFile(filepath.Clean(path)) if err != nil { c.errors = append(c.errors, fmt.Errorf("%w: %w", ErrCannotOpenYAMLFile, err)) return c } c.readFromBytes(bytes) return c } // FromString loads a YAML file from the given string. func (c *Container) FromString(input string) *Container { c.readFromBytes([]byte(input)) return c } // FromReader loads a YAML file from a reader stream. func (c *Container) FromReader(reader io.Reader) *Container { if reader == nil { c.errors = append(c.errors, ErrCannotReadYAMLData) return c } buffer := new(bytes.Buffer) if _, err := buffer.ReadFrom(reader); err != nil { c.errors = append(c.errors, fmt.Errorf("%w: %w", ErrCannotReadYAMLData, err)) return c } c.readFromBytes(buffer.Bytes()) return c } // FromBytes loads a YAML file from the given bytes. func (c *Container) FromBytes(input []byte) *Container { c.readFromBytes(input) return c } func (c *Container) readFromBytes(input []byte) { var value any if err := yaml.Unmarshal(input, &value); err != nil { c.errors = append(c.errors, fmt.Errorf("%w: %w", ErrCannotReadYAMLBytes, err)) return } c.values = append(c.values, value) }
package confiq // ConfigSetOptions is exposed so that functions which wrap the New function can make adding the WithTag option easier. type ConfigSetOptions []loadOption type configSetOption func(*ConfigSet) // WithTag sets the struct tag to be used by the decoder for reading configuration values of struct fields. func WithTag(tag string) configSetOption { return func(s *ConfigSet) { s.decoder.tag = tag } } // LoadOptions is exposed so that functions which wrap the Load function can make adding the WithPrefix option easier. type LoadOptions []loadOption type loadOption func(*loader) // WithPrefix sets the prefix to be used when loading configuration values into the ConfigSet. func WithPrefix(prefix string) loadOption { return func(l *loader) { l.prefix = prefix } } // DecodeOptions is exposed so that functions which wrap the Decode function can make adding the AsStrict and FromPrefix options easier. type DecodeOptions []decodeOption type decodeOption func(*decodeSettings) // AsStrict sets the decoder to decode the configuration values as if all fields are set to strict. func AsStrict() decodeOption { return func(d *decodeSettings) { d.strict = true } } // FromPrefix sets the prefix to be used when decoding configuration values into the target struct. func FromPrefix(prefix string) decodeOption { return func(d *decodeSettings) { d.prefix = prefix } }
package confiq import ( "errors" "fmt" "reflect" "strconv" ) var ( errCannotParseBool = errors.New("cannot parse bool") errCannotParseFloat = errors.New("cannot parse float") errCannotParseInt = errors.New("cannot parse int") errCannotParseUint = errors.New("cannot parse uint") ) func decodeString(targetValue reflect.Value, sourceValue any) error { targetValue.SetString(castToString(sourceValue)) return nil } func decodeBool(targetValue reflect.Value, sourceValue any) error { if boolValue, ok := sourceValue.(bool); ok { targetValue.SetBool(boolValue) return nil } else { parsedBool, err := strconv.ParseBool(castToString(sourceValue)) if err != nil { return fmt.Errorf("%w: %w", errCannotParseBool, err) } targetValue.SetBool(parsedBool) } return nil } func decodeFloat(targetValue reflect.Value, sourceValue any) error { switch sV := sourceValue.(type) { case float32: targetValue.SetFloat(float64(sV)) case float64: targetValue.SetFloat(sV) default: parsedFloat, err := strconv.ParseFloat(castToString(sourceValue), targetValue.Type().Bits()) if err != nil { return fmt.Errorf("%w: %w", errCannotParseFloat, err) } targetValue.SetFloat(parsedFloat) } return nil } func decodeInt(targetValue reflect.Value, sourceValue any) error { switch sV := sourceValue.(type) { case int: targetValue.SetInt(int64(sV)) case int8: targetValue.SetInt(int64(sV)) case int16: targetValue.SetInt(int64(sV)) case int32: targetValue.SetInt(int64(sV)) case int64: targetValue.SetInt(sV) default: parsedInt, err := strconv.ParseInt(castToString(sourceValue), 0, targetValue.Type().Bits()) if err != nil { return fmt.Errorf("%w: %w", errCannotParseInt, err) } targetValue.SetInt(parsedInt) } return nil } func decodeUint(targetValue reflect.Value, sourceValue any) error { switch sV := sourceValue.(type) { case uint: targetValue.SetUint(uint64(sV)) case uint8: targetValue.SetUint(uint64(sV)) case uint16: targetValue.SetUint(uint64(sV)) case uint32: targetValue.SetUint(uint64(sV)) case uint64: targetValue.SetUint(sV) default: parsedUint, err := strconv.ParseUint(castToString(sourceValue), 0, targetValue.Type().Bits()) if err != nil { return fmt.Errorf("%w: %w", errCannotParseUint, err) } targetValue.SetUint(parsedUint) } return nil }
package confiq import ( "strconv" "strings" ) const ( segmentDividerChar = "." openBraceChar = "[" closeBraceChar = "]" ) type segment interface { String() string } type keySegment string func (kS keySegment) String() string { return string(kS) } func (kS keySegment) asString() string { return string(kS) } type indexSegment int func (iS indexSegment) String() string { return openBraceChar + strconv.Itoa(int(iS)) + closeBraceChar } func (iS indexSegment) asInt() int { return int(iS) } func getNextSegment(path string) (segment, string) { var ( nextSegment, remainingPath string segmentDividerIndex = strings.Index(path, segmentDividerChar) ) if segmentDividerIndex == -1 { nextSegment = path remainingPath = "" } else { nextSegment = path[:segmentDividerIndex] remainingPath = path[segmentDividerIndex+1:] } openBraceIndex := strings.Index(path, openBraceChar) if openBraceIndex == -1 { return keySegment(nextSegment), remainingPath } closeBraceIndex := strings.Index(nextSegment, closeBraceChar) if closeBraceIndex == -1 || closeBraceIndex < openBraceIndex { return keySegment(nextSegment), remainingPath } if openBraceIndex != 0 { return keySegment(nextSegment[:openBraceIndex]), nextSegment[openBraceIndex:closeBraceIndex+1] + segmentDividerChar + remainingPath } index := nextSegment[openBraceIndex+1 : closeBraceIndex] if indexInt, err := strconv.Atoi(index); err == nil { return indexSegment(indexInt), remainingPath } return keySegment(index), remainingPath }