From 8a47b7086e8a143c891621886a2414c97464d553 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B3=9B=E7=BA=A2=E6=A9=98=E5=AD=90?= Date: Sat, 27 Jul 2024 22:00:53 +0800 Subject: [PATCH] first commit --- .env | 11 +++ go.mod | 3 + parser.go | 230 +++++++++++++++++++++++++++++++++++++++++++++++++ parser_test.go | 46 ++++++++++ 4 files changed, 290 insertions(+) create mode 100644 .env create mode 100644 go.mod create mode 100644 parser.go create mode 100644 parser_test.go diff --git a/.env b/.env new file mode 100644 index 0000000..0f704df --- /dev/null +++ b/.env @@ -0,0 +1,11 @@ +# server +apiport = :8080 + +# database +user = root +password = "12345678" + +host = 127.0.0.1 +port = 3306 + +db_name = 'test' \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..77b6801 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/bingxio/dotenv + +go 1.22.5 diff --git a/parser.go b/parser.go new file mode 100644 index 0000000..353f06d --- /dev/null +++ b/parser.go @@ -0,0 +1,230 @@ +package dotenv + +import ( + "errors" + "fmt" + "reflect" + "strconv" + "strings" + "unicode" +) + +type Kv struct { + Key string + Value string +} + +type KvSlice []Kv + +func (s KvSlice) Get(key string) (string, bool) { + for _, v := range s { + if v.Key == key { + return v.Value, true + } + } + return "", false +} + +var ( + buffer []byte = nil + line = 1 + ip = 0 +) + +func makeError(text string) error { return fmt.Errorf(text, line) } +func end() bool { return ip >= len(buffer) } + +func now() byte { + if ip >= len(buffer) { + return '\x00' + } + return buffer[ip] +} + +func isLetter() bool { + c := now() + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' +} + +func whiteSpace() { + for now() == ' ' { + ip++ + } + for now() == '\n' { + ip++ + line++ + } +} + +func markComment() { + if now() == '#' { + for !end() && now() != '\n' { + ip++ + } + ip++ + line++ + } +} + +func markKeyValue() (*Kv, error) { + key := []byte{} + + for isLetter() { + key = append(key, now()) + ip++ + } + if len(key) == 0 { + return nil, makeError("key is empty (line %d)") + } + whiteSpace() + + if now() != '=' { + return nil, makeError("expect equal(=) sign after key at line %d") + } + ip++ + if now() == '\n' || end() { + return nil, makeError("value is empty (line %d)") + } + whiteSpace() + + value := []byte{} + + for !end() && now() != '\n' { + if now() != '"' && now() != '\'' { + value = append(value, now()) + } + ip++ + } + line++ + + kv := Kv{ + Key: string(key), + Value: string(value), + } + return &kv, nil +} + +func unmarshal() (KvSlice, error) { + slice := KvSlice{} + + for !end() { + if whiteSpace(); ip >= len(buffer) { + return slice, nil + } + if markComment(); ip >= len(buffer) { + return slice, nil + } + v, err := markKeyValue() + if err != nil { + return nil, err + } + slice = append(slice, *v) + ip++ + } + return slice, nil +} + +func restore(src []byte) { + buffer = src + line = 1 + ip = 0 +} + +func Unmarshal(src []byte, dst any) error { + restore(src) + slice, err := unmarshal() + + if err != nil { + return err + } + v := reflect.ValueOf(dst) + + if v.Kind() != reflect.Ptr || v.IsNil() { + return errors.New("encoding structure type is incorrect") + } + v = v.Elem() // value pointed to by the pointer + + for i := 0; i < v.NumField(); i++ { + fieldType := v.Type().Field(i) + fieldValue := v.Field(i) + + if !unicode.IsUpper(rune(fieldType.Name[0])) { + continue + } + err := encodeToStruct(slice, fieldType, fieldValue) + if err != nil { + return err + } + } + return nil +} + +func UnmarshalSlice(src []byte) (KvSlice, error) { + restore(src) + return unmarshal() +} + +func encodeToStruct( + slice KvSlice, + ft reflect.StructField, + fv reflect.Value, +) error { + var ok bool + var value string + err := fmt.Errorf("undefined key '%s'", ft.Name) + + if ft.Tag == "" { + value, ok = slice.Get(strings.ToLower(ft.Name)) + if !ok { + value, ok = slice.Get(strings.ToUpper(ft.Name)) + } + if !ok { + return err + } + } else { + value, ok = slice.Get(ft.Tag.Get("env")) + if !ok { + return err + } + } + return valueType(value, ft.Type, fv) +} + +func valueType(value string, p reflect.Type, val reflect.Value) error { + switch p.Kind() { + // string + case reflect.String: + val.SetString(value) + return nil + // uint + case reflect.Uint, + reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + + v, err := strconv.ParseUint(value, 0, 64) + if err != nil { + return err + } + val.SetUint(v) + return nil + // int + case reflect.Int, + reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + + v, err := strconv.ParseInt(value, 0, 64) + if err != nil { + return err + } + val.SetInt(v) + return nil + // float + case reflect.Float32, reflect.Float64: + + v, err := strconv.ParseFloat(value, 64) + if err != nil { + return err + } + val.SetFloat(v) + return nil + } + return errors.New("only supports parsing number and string") +} diff --git a/parser_test.go b/parser_test.go new file mode 100644 index 0000000..d929ece --- /dev/null +++ b/parser_test.go @@ -0,0 +1,46 @@ +package dotenv + +import ( + "fmt" + "os" + "testing" +) + +type Config struct { + ApiPort string + User string + Password string + Host string + Port int + Db string `env:"db_name"` +} + +func TestParser(t *testing.T) { + var conf Config + + buffer, err := os.ReadFile(".env") + if err != nil { + t.Fatal(err) + } + if err := Unmarshal(buffer, &conf); err != nil { + t.Fatal(err) + } + fmt.Printf("conf: %v\n", conf) + + slice, err := UnmarshalSlice(buffer) + if err != nil { + t.Fatal(err) + } + for _, v := range slice { + fmt.Println(v.Key, "=>", v.Value) + } + value, ok := slice.Get("user") + if ok { + fmt.Printf("value: %v\n", value) + } + + _, ok = slice.Get("other") + if ok { + t.Fatal("inappropriate value") + } +}