Skip to content

Commit

Permalink
Merge pull request #5 from Arkoniak/dotenvx
Browse files Browse the repository at this point in the history
multiple refactorings
  • Loading branch information
Arkoniak authored Mar 27, 2021
2 parents 685f400 + 85ff53f commit ad05f7b
Show file tree
Hide file tree
Showing 11 changed files with 334 additions and 129 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "ConfigEnv"
uuid = "01234589-554d-41b7-ae5c-7b6ee2db6796"
authors = ["Andrey Oskin <[email protected]>"]
version = "0.1.0"
version = "0.2.0"

[deps]

Expand Down
126 changes: 104 additions & 22 deletions src/ConfigEnv.jl
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
module ConfigEnv

import Base: getindex, get, isempty
import Base: getindex, get, isempty, merge, merge!, haskey

export dotenv
export dotenv, dotenvx

struct EnvProxyDict
dict::Dict{String, String}
Expand All @@ -11,10 +11,19 @@ end
getindex(ed::EnvProxyDict, key) = get(ed.dict, key, ENV[key])
get(ed::EnvProxyDict, key, default) = get(ed.dict, key, get(ENV, key, default))
isempty(ed::EnvProxyDict) = isempty(ed.dict)
merge(epd1::EnvProxyDict, epd2::EnvProxyDict...) = EnvProxyDict(foldl((x, y) -> merge(x, y.dict), epd2; init = epd1.dict))
haskey(ed::EnvProxyDict, key) = haskey(ed.dict, key) || haskey(ENV, key)

function merge!(epd1::EnvProxyDict, epd2::EnvProxyDict...)
foldl((x, y) -> merge!(x, y.dict), epd2; init = epd1.dict)
epd1
end

Base.:*(epd1::EnvProxyDict, epd2::EnvProxyDict...) = EnvProxyDict(foldl((x, y) -> merge(x, y.dict), epd2; init = epd1.dict))

"""
`ConfigEnv.parse` accepts a String or an IOBuffer (any value that
can be converted into String), and it return a Dict with
can be converted into String), and returns a Dict with
the parsed keys and values.
"""
function parse(src)
Expand All @@ -40,32 +49,105 @@ function parse(src)
res
end

parse(src::IO) = parse(String(take!(src)))

"""
dotenv(path, override = true)
dotenv(; path = ".env", override = true)
########################################
# Main part
########################################

`dotenv` reads your `path` .env file, parse the content, stores it to `ENV`,
and finally return a `EnvProxyDict` with the content. By default if key already exists in
`ENV` it is overriden with the values in .env file. This behaviour can be changed by
setting `override` flag to `false`.
validatefile(s) = false
validatefile(s::AbstractString) = isfile(s)

"""
dotenv(path1, path2, ...; overwrite = true)
`dotenv` reads .env files from your `path`, parse their content, merge them together, stores result to `ENV`,
and finally return a `EnvProxyDict` with the content. If no `path` argument is given , then
`.env` is used as a default path. During merge procedure, if duplicate keys encountered
then value from the rightmost dictionary is used.
By default if key already exists in `ENV` it is overwritten with the values in .env file.
This behaviour can be changed by setting `overwrite` flag to `false` or using complementary `dotenvx` function.
Examples
========
```
# .env
FOO = bar
USER = john_doe
# julia REPL
# load key-value pairs from ".env", `ENV` duplicate keys are overwritten
julia> ENV["USER"]
user1
julia> cfg = dotenvx()
julia> ENV["FOO"]
bar
julia> ENV["USER"]
john_doe
julia> cfg["USER"]
john_doe
# loading multiple files simultaneously
cfg = dotenv(".env1", ".env2")
# alternatively
cfg = dotenv([".env1", ".env2"]...)
```
"""
function dotenv(path, override = true)
if (isfile(path))
parsed = parse(read(path, String))
function dotenv(path = ".env"; overwrite = true)
parsed = if (validatefile(path))
parse(read(path, String))
else
parse(path)
end

for (k, v) in parsed
if !haskey(ENV, k) || override
ENV[k] = v
end
for (k, v) in parsed
if !haskey(ENV, k) || overwrite
ENV[k] = v
end

return EnvProxyDict(parsed)
else
return EnvProxyDict(Dict{String, String}())
end

return EnvProxyDict(parsed)
end

dotenv(;path=".env", override = true) = config(path, override)
dotenv(paths...; overwrite = true) = merge!(dotenv.(paths; overwrite = overwrite)...)

"""
dotenvx(path1, path2, ...; overwrite = false)
`dotenvx` reads .env files from your `path`, parse their content, merge them together, stores result to `ENV`,
and finally return a `EnvProxyDict` with the content. If no `path` argument is given , then
`.env` is used as a default path. During merge procedure, if duplicate keys encountered
then value from the rightmost dictionary is used.
By default if key already exists in `ENV` it is overwritten with the values in .env file.
This behaviour can be changed by setting `overwrite` flag to `true` or using complementary `dotenv` function.
Examples
========
```
# .env
FOO = bar
USER = john_doe
# julia REPL
# load key-value pairs from ".env", `ENV` duplicate keys are not overwritten
julia> ENV["USER"]
user1
julia> cfg = dotenvx()
julia> ENV["FOO"]
bar
julia> ENV["USER"]
user1
julia> cfg["USER"]
john_doe
# loading multiple files simultaneously
cfg = dotenvx(".env1", ".env2")
# alternatively
cfg = dotenvx([".env1", ".env2"]...)
```
"""
dotenvx(paths...; overwrite = false) = dotenv(paths...; overwrite = overwrite)

end
15 changes: 1 addition & 14 deletions test/.env
Original file line number Diff line number Diff line change
@@ -1,14 +1 @@
TEST=OK
#COMMENT=YES
BAD= asd asd asd asd \n asd asd something

OTHERVAR=asd
EMPTY=
USER=john

CONCOMILLAS="something with spac\nes"
CONCOMIL='something with spac\nes'
JSON={"key":"value"}

CON_ESPACIOS1= something
CON_ESPACIOS2=" something "
QWERTY = ZXC
1 change: 1 addition & 0 deletions test/.env1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FOO = BAR
1 change: 1 addition & 0 deletions test/.env2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
QWE = ASD
1 change: 1 addition & 0 deletions test/.env3
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FOO = FUU
14 changes: 14 additions & 0 deletions test/.testenv
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
TEST=OK
#COMMENT=YES
BAD= asd asd asd asd \n asd asd something

OTHERVAR=asd
EMPTY=
USER=foo

DOUBLEQUOTES="something with spac\nes"
SINGLEQOUTES='something with spac\nes'
JSON={"key":"value"}

MANY_SPACES= something
QUOTED_SPACES=" something "
111 changes: 19 additions & 92 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,101 +1,28 @@
module TestDotEnv

using ConfigEnv

using Test

const dir = dirname(@__FILE__)

# There is no "USER" variable on windows.
initial_value = haskey(ENV, "USER") ? ENV["USER"] : "WINDOWS"
ENV["USER"] = initial_value

@testset "basic" begin
# basic input
str = "BASIC=basic"
file = joinpath(dir, ".env.override")
file2 = joinpath(dir, ".env")

# can turn off override of ENV vars
cfg = dotenv(file, false)
@test ENV["USER"] != cfg["USER"]
@test ENV["USER"] == initial_value

# iobuffer, string, file
@test ConfigEnv.parse(str) == Dict("BASIC"=>"basic")
@test ConfigEnv.parse(read(file)) == Dict("CUSTOMVAL123"=>"yes","USER"=>"replaced value")
@test dotenv(file).dict == Dict("CUSTOMVAL123"=>"yes","USER"=>"replaced value")

@test isempty(dotenv("inexistentfile.env"))

# length of returned values
@test length(dotenv(file2).dict) == 10

# appropiately loaded into ENV if CUSTOM_VAL is non existent
@test ENV["CUSTOMVAL123"] == "yes"

# Test that EnvProxyDict is reading from ENV
ENV["SOME_RANDOM_KEY"] = "abc"
cfg = dotenv(file)
@test !haskey(cfg.dict, "SOME_RANDOM_KEY")
@test cfg["SOME_RANDOM_KEY"] == "abc"
@test get(cfg, "OTHER_RANDOM_KEY", "zxc") == "zxc"
end

@testset "Override" begin
# basic input
file = joinpath(dir, ".env.override")

# By default force override
cfg = dotenv(file)
@test ENV["USER"] == cfg["USER"]
@test ENV["USER"] == "replaced value"

# Restore previous environment
ENV["USER"] = initial_value
end

@testset "parsing" begin

#comment
@test ConfigEnv.parse("#HIMOM") == Dict()

#spaces without quotes
@test begin
p = ConfigEnv.parse("TEST=hi the re")
count(c -> c == ' ', collect(p["TEST"])) == 4
@testset "Global" begin
for file in sort([file for file in readdir(@__DIR__) if
occursin(r"^test[_0-9]+.*\.jl$", file)])
m = match(r"test([0-9]+)_(.*).jl", file)
filename = String(m[2])
testnum = string(parse(Int, m[1]))

# with this test one can run only specific tests, for example
# Pkg.test("TimeZoneLookup", test_args = ["xxx"])
# or
# Pkg.test("TimeZoneLookup", test_args = ["1"])
if isempty(ARGS) || (filename in ARGS) || (testnum in ARGS) || (m[1] in ARGS)
@testset "$filename" begin
# Here you can optionally exclude some test files
# VERSION < v"1.1" && file == "test_xxx.jl" && continue

include(file)
end
end
end

#single quotes
@test ConfigEnv.parse("TEST=''")["TEST"] == ""
@test ConfigEnv.parse("TEST='something'")["TEST"] == "something"

#double quotes
@test ConfigEnv.parse("TEST=\"\"")["TEST"] == ""
@test ConfigEnv.parse("TEST=\"something\"")["TEST"] == "something"

#inner quotes are mantained
@test ConfigEnv.parse("TEST=\"\"json\"\"")["TEST"] == "\"json\""
@test ConfigEnv.parse("TEST=\"'json'\"")["TEST"] == "'json'"
@test ConfigEnv.parse("TEST=\"\"\"")["TEST"] == "\""
@test ConfigEnv.parse("TEST=\"'\"")["TEST"] == "'"

#line breaks
@test ConfigEnv.parse("TEST=\"\\n\"")["TEST"] == "" #It's null because of final trim
@test ConfigEnv.parse("TEST=\"\\n\\n\\nsomething\"")["TEST"] == "something"
@test ConfigEnv.parse("TEST=\"something\\nsomething\"")["TEST"] == "something\nsomething"
@test ConfigEnv.parse("TEST=\"something\\n\\nsomething\"")["TEST"] == "something\n\nsomething"
@test ConfigEnv.parse("TEST='\\n'")["TEST"] == "\\n"
@test ConfigEnv.parse("TEST=\\n")["TEST"] == "\\n"

#empty vars
@test ConfigEnv.parse("TEST=")["TEST"] == ""

#trim spaces with and without quotes
@test ConfigEnv.parse("TEST=' something '")["TEST"] == "something"
@test ConfigEnv.parse("TEST=\" something \"")["TEST"] == "something"
@test ConfigEnv.parse("TEST= something ")["TEST"] == "something"
@test ConfigEnv.parse("TEST= ")["TEST"] == ""
end

end # module
Loading

0 comments on commit ad05f7b

Please sign in to comment.