- graph, dotenv read
- graph, cors and secure header - web, urql client basic setup
This commit is contained in:
		
							
								
								
									
										78
									
								
								config/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								config/config.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | ||||
| package config | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"reflect" | ||||
| 	"strconv" | ||||
|  | ||||
| 	"gitserver.in/patialtech/rano/config/dotenv" | ||||
| 	"gitserver.in/patialtech/rano/pkg/logger" | ||||
| ) | ||||
|  | ||||
| const Env = "development" | ||||
|  | ||||
| var conf *Config | ||||
|  | ||||
| func init() { | ||||
| 	envVar, err := dotenv.Read(fmt.Sprintf(".env.%s", Env)) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
|  | ||||
| 	conf = &Config{} | ||||
| 	conf.loadEnv(envVar) | ||||
| } | ||||
|  | ||||
| // Read config for Env | ||||
| func Read() *Config { | ||||
| 	if conf == nil { | ||||
| 		panic("config not initialized") | ||||
| 	} | ||||
|  | ||||
| 	return conf | ||||
| } | ||||
|  | ||||
| type Config struct { | ||||
| 	WebPort   int    `env:"WEB_PORT"` | ||||
| 	WebURL    string `env:"WEB_URL"` | ||||
| 	GraphPort int    `env:"GRAPH_PORT"` | ||||
| 	GrapURL   string `env:"GRAPH_URL"` | ||||
| } | ||||
|  | ||||
| func (c *Config) loadEnv(vars map[string]string) { | ||||
| 	if c == nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	val := reflect.Indirect(reflect.ValueOf(c)) | ||||
| 	for i := 0; i < val.NumField(); i++ { | ||||
| 		f := val.Type().Field(i) | ||||
| 		tag := f.Tag.Get("env") | ||||
| 		if tag == "" { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		v, found := vars[tag] | ||||
| 		if !found { | ||||
| 			logger.Warn("var %q not found in file .env.%s", tag, Env) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		field := val.FieldByName(f.Name) | ||||
| 		if !field.IsValid() { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		switch f.Type.Kind() { | ||||
| 		case reflect.Int: | ||||
| 			if intV, err := strconv.ParseInt(v, 10, 64); err != nil { | ||||
| 				panic(err) | ||||
| 			} else { | ||||
| 				field.SetInt(intV) | ||||
| 			} | ||||
| 		default: | ||||
| 			field.SetString(v) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| } | ||||
							
								
								
									
										282
									
								
								config/dotenv/parser.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										282
									
								
								config/dotenv/parser.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,282 @@ | ||||
| package dotenv | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"unicode" | ||||
| ) | ||||
|  | ||||
| // | ||||
| // copied from https://github.com/joho/godotenv/blob/main/parser.go | ||||
| // | ||||
|  | ||||
| const ( | ||||
| 	charComment       = '#' | ||||
| 	prefixSingleQuote = '\'' | ||||
| 	prefixDoubleQuote = '"' | ||||
|  | ||||
| 	exportPrefix = "export" | ||||
| ) | ||||
|  | ||||
| func parseBytes(src []byte, out map[string]string) error { | ||||
| 	src = bytes.Replace(src, []byte("\r\n"), []byte("\n"), -1) | ||||
| 	cutset := src | ||||
| 	for { | ||||
| 		cutset = getStatementStart(cutset) | ||||
| 		if cutset == nil { | ||||
| 			// reached end of file | ||||
| 			break | ||||
| 		} | ||||
|  | ||||
| 		key, left, err := locateKeyName(cutset) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		value, left, err := extractVarValue(left, out) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		out[key] = value | ||||
| 		cutset = left | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // getStatementPosition returns position of statement begin. | ||||
| // | ||||
| // It skips any comment line or non-whitespace character. | ||||
| func getStatementStart(src []byte) []byte { | ||||
| 	pos := indexOfNonSpaceChar(src) | ||||
| 	if pos == -1 { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	src = src[pos:] | ||||
| 	if src[0] != charComment { | ||||
| 		return src | ||||
| 	} | ||||
|  | ||||
| 	// skip comment section | ||||
| 	pos = bytes.IndexFunc(src, isCharFunc('\n')) | ||||
| 	if pos == -1 { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	return getStatementStart(src[pos:]) | ||||
| } | ||||
|  | ||||
| // locateKeyName locates and parses key name and returns rest of slice | ||||
| func locateKeyName(src []byte) (key string, cutset []byte, err error) { | ||||
| 	// trim "export" and space at beginning | ||||
| 	src = bytes.TrimLeftFunc(src, isSpace) | ||||
| 	if bytes.HasPrefix(src, []byte(exportPrefix)) { | ||||
| 		trimmed := bytes.TrimPrefix(src, []byte(exportPrefix)) | ||||
| 		if bytes.IndexFunc(trimmed, isSpace) == 0 { | ||||
| 			src = bytes.TrimLeftFunc(trimmed, isSpace) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// locate key name end and validate it in single loop | ||||
| 	offset := 0 | ||||
| loop: | ||||
| 	for i, char := range src { | ||||
| 		rchar := rune(char) | ||||
| 		if isSpace(rchar) { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		switch char { | ||||
| 		case '=', ':': | ||||
| 			// library also supports yaml-style value declaration | ||||
| 			key = string(src[0:i]) | ||||
| 			offset = i + 1 | ||||
| 			break loop | ||||
| 		case '_': | ||||
| 		default: | ||||
| 			// variable name should match [A-Za-z0-9_.] | ||||
| 			if unicode.IsLetter(rchar) || unicode.IsNumber(rchar) || rchar == '.' { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			return "", nil, fmt.Errorf( | ||||
| 				`unexpected character %q in variable name near %q`, | ||||
| 				string(char), string(src)) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if len(src) == 0 { | ||||
| 		return "", nil, errors.New("zero length string") | ||||
| 	} | ||||
|  | ||||
| 	// trim whitespace | ||||
| 	key = strings.TrimRightFunc(key, unicode.IsSpace) | ||||
| 	cutset = bytes.TrimLeftFunc(src[offset:], isSpace) | ||||
| 	return key, cutset, nil | ||||
| } | ||||
|  | ||||
| // extractVarValue extracts variable value and returns rest of slice | ||||
| func extractVarValue(src []byte, vars map[string]string) (value string, rest []byte, err error) { | ||||
| 	quote, hasPrefix := hasQuotePrefix(src) | ||||
| 	if !hasPrefix { | ||||
| 		// unquoted value - read until end of line | ||||
| 		endOfLine := bytes.IndexFunc(src, isLineEnd) | ||||
|  | ||||
| 		// Hit EOF without a trailing newline | ||||
| 		if endOfLine == -1 { | ||||
| 			endOfLine = len(src) | ||||
|  | ||||
| 			if endOfLine == 0 { | ||||
| 				return "", nil, nil | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Convert line to rune away to do accurate countback of runes | ||||
| 		line := []rune(string(src[0:endOfLine])) | ||||
|  | ||||
| 		// Assume end of line is end of var | ||||
| 		endOfVar := len(line) | ||||
| 		if endOfVar == 0 { | ||||
| 			return "", src[endOfLine:], nil | ||||
| 		} | ||||
|  | ||||
| 		// Work backwards to check if the line ends in whitespace then | ||||
| 		// a comment (ie asdasd # some comment) | ||||
| 		for i := endOfVar - 1; i >= 0; i-- { | ||||
| 			if line[i] == charComment && i > 0 { | ||||
| 				if isSpace(line[i-1]) { | ||||
| 					endOfVar = i | ||||
| 					break | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		trimmed := strings.TrimFunc(string(line[0:endOfVar]), isSpace) | ||||
|  | ||||
| 		return expandVariables(trimmed, vars), src[endOfLine:], nil | ||||
| 	} | ||||
|  | ||||
| 	// lookup quoted string terminator | ||||
| 	for i := 1; i < len(src); i++ { | ||||
| 		if char := src[i]; char != quote { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// skip escaped quote symbol (\" or \', depends on quote) | ||||
| 		if prevChar := src[i-1]; prevChar == '\\' { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// trim quotes | ||||
| 		trimFunc := isCharFunc(rune(quote)) | ||||
| 		value = string(bytes.TrimLeftFunc(bytes.TrimRightFunc(src[0:i], trimFunc), trimFunc)) | ||||
| 		if quote == prefixDoubleQuote { | ||||
| 			// unescape newlines for double quote (this is compat feature) | ||||
| 			// and expand environment variables | ||||
| 			value = expandVariables(expandEscapes(value), vars) | ||||
| 		} | ||||
|  | ||||
| 		return value, src[i+1:], nil | ||||
| 	} | ||||
|  | ||||
| 	// return formatted error if quoted string is not terminated | ||||
| 	valEndIndex := bytes.IndexFunc(src, isCharFunc('\n')) | ||||
| 	if valEndIndex == -1 { | ||||
| 		valEndIndex = len(src) | ||||
| 	} | ||||
|  | ||||
| 	return "", nil, fmt.Errorf("unterminated quoted value %s", src[:valEndIndex]) | ||||
| } | ||||
|  | ||||
| func expandEscapes(str string) string { | ||||
| 	out := escapeRegex.ReplaceAllStringFunc(str, func(match string) string { | ||||
| 		c := strings.TrimPrefix(match, `\`) | ||||
| 		switch c { | ||||
| 		case "n": | ||||
| 			return "\n" | ||||
| 		case "r": | ||||
| 			return "\r" | ||||
| 		default: | ||||
| 			return match | ||||
| 		} | ||||
| 	}) | ||||
| 	return unescapeCharsRegex.ReplaceAllString(out, "$1") | ||||
| } | ||||
|  | ||||
| func indexOfNonSpaceChar(src []byte) int { | ||||
| 	return bytes.IndexFunc(src, func(r rune) bool { | ||||
| 		return !unicode.IsSpace(r) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // hasQuotePrefix reports whether charset starts with single or double quote and returns quote character | ||||
| func hasQuotePrefix(src []byte) (prefix byte, isQuored bool) { | ||||
| 	if len(src) == 0 { | ||||
| 		return 0, false | ||||
| 	} | ||||
|  | ||||
| 	switch prefix := src[0]; prefix { | ||||
| 	case prefixDoubleQuote, prefixSingleQuote: | ||||
| 		return prefix, true | ||||
| 	default: | ||||
| 		return 0, false | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func isCharFunc(char rune) func(rune) bool { | ||||
| 	return func(v rune) bool { | ||||
| 		return v == char | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // isSpace reports whether the rune is a space character but not line break character | ||||
| // | ||||
| // this differs from unicode.IsSpace, which also applies line break as space | ||||
| func isSpace(r rune) bool { | ||||
| 	switch r { | ||||
| 	case '\t', '\v', '\f', '\r', ' ', 0x85, 0xA0: | ||||
| 		return true | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| func isLineEnd(r rune) bool { | ||||
| 	if r == '\n' || r == '\r' { | ||||
| 		return true | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	escapeRegex        = regexp.MustCompile(`\\.`) | ||||
| 	expandVarRegex     = regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`) | ||||
| 	unescapeCharsRegex = regexp.MustCompile(`\\([^$])`) | ||||
| ) | ||||
|  | ||||
| func expandVariables(v string, m map[string]string) string { | ||||
| 	return expandVarRegex.ReplaceAllStringFunc(v, func(s string) string { | ||||
| 		submatch := expandVarRegex.FindStringSubmatch(s) | ||||
|  | ||||
| 		if submatch == nil { | ||||
| 			return s | ||||
| 		} | ||||
| 		if submatch[1] == "\\" || submatch[2] == "(" { | ||||
| 			return submatch[0][1:] | ||||
| 		} else if submatch[4] != "" { | ||||
| 			if val, ok := m[submatch[4]]; ok { | ||||
| 				return val | ||||
| 			} | ||||
| 			if val, ok := os.LookupEnv(submatch[4]); ok { | ||||
| 				return val | ||||
| 			} | ||||
| 			return m[submatch[4]] | ||||
| 		} | ||||
| 		return s | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										69
									
								
								config/dotenv/read.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								config/dotenv/read.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| package dotenv | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"io" | ||||
| 	"os" | ||||
| ) | ||||
|  | ||||
| // | ||||
| // copied from https://github.com/joho/godotenv/blob/main/godotenv.go | ||||
| // | ||||
|  | ||||
| // Read all env (with same file loading semantics as Load) but return values as | ||||
| // a map rather than automatically writing values into env | ||||
| func Read(filenames ...string) (envMap map[string]string, err error) { | ||||
| 	filenames = filenamesOrDefault(filenames) | ||||
| 	envMap = make(map[string]string) | ||||
|  | ||||
| 	for _, filename := range filenames { | ||||
| 		individualEnvMap, individualErr := readFile(filename) | ||||
|  | ||||
| 		if individualErr != nil { | ||||
| 			err = individualErr | ||||
| 			return // return early on a spazout | ||||
| 		} | ||||
|  | ||||
| 		for key, value := range individualEnvMap { | ||||
| 			envMap[key] = value | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func filenamesOrDefault(filenames []string) []string { | ||||
| 	if len(filenames) == 0 { | ||||
| 		return []string{".env"} | ||||
| 	} | ||||
| 	return filenames | ||||
| } | ||||
|  | ||||
| func readFile(filename string) (envMap map[string]string, err error) { | ||||
| 	file, err := os.Open(filename) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	defer file.Close() | ||||
|  | ||||
| 	return parse(file) | ||||
| } | ||||
|  | ||||
| // parse reads an env file from io.Reader, returning a map of keys and values. | ||||
| func parse(r io.Reader) (map[string]string, error) { | ||||
| 	var buf bytes.Buffer | ||||
| 	_, err := io.Copy(&buf, r) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return unmarshal(buf.Bytes()) | ||||
| } | ||||
|  | ||||
| // unmarshal parses env file from byte slice of chars, returning a map of keys and values. | ||||
| func unmarshal(src []byte) (map[string]string, error) { | ||||
| 	out := make(map[string]string) | ||||
| 	err := parseBytes(src, out) | ||||
|  | ||||
| 	return out, err | ||||
| } | ||||
							
								
								
									
										62
									
								
								config/dotenv/write.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								config/dotenv/write.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| package dotenv | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"sort" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| // | ||||
| // copied from https://github.com/joho/godotenv/blob/main/godotenv.go | ||||
| // | ||||
|  | ||||
| const doubleQuoteSpecialChars = "\\\n\r\"!$`" | ||||
|  | ||||
| // Write serializes the given environment and writes it to a file. | ||||
| func Write(envMap map[string]string, filename string) error { | ||||
| 	content, err := marshal(envMap) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	file, err := os.Create(filename) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer file.Close() | ||||
| 	_, err = file.WriteString(content + "\n") | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return file.Sync() | ||||
| } | ||||
|  | ||||
| // marshal outputs the given environment as a dotenv-formatted environment file. | ||||
| // Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped. | ||||
| func marshal(envMap map[string]string) (string, error) { | ||||
| 	lines := make([]string, 0, len(envMap)) | ||||
| 	for k, v := range envMap { | ||||
| 		if d, err := strconv.Atoi(v); err == nil { | ||||
| 			lines = append(lines, fmt.Sprintf(`%s=%d`, k, d)) | ||||
| 		} else { | ||||
| 			lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v))) | ||||
| 		} | ||||
| 	} | ||||
| 	sort.Strings(lines) | ||||
| 	return strings.Join(lines, "\n"), nil | ||||
| } | ||||
|  | ||||
| func doubleQuoteEscape(line string) string { | ||||
| 	for _, c := range doubleQuoteSpecialChars { | ||||
| 		toReplace := "\\" + string(c) | ||||
| 		if c == '\n' { | ||||
| 			toReplace = `\n` | ||||
| 		} | ||||
| 		if c == '\r' { | ||||
| 			toReplace = `\r` | ||||
| 		} | ||||
| 		line = strings.Replace(line, string(c), toReplace, -1) | ||||
| 	} | ||||
| 	return line | ||||
| } | ||||
		Reference in New Issue
	
	Block a user