Skip to content

Commit

Permalink
Merge pull request #10 from Arkoniak/interpolations
Browse files Browse the repository at this point in the history
interpolations implementation
  • Loading branch information
Arkoniak authored Mar 31, 2021
2 parents 66d88b9 + e3d42e9 commit 18dffd5
Show file tree
Hide file tree
Showing 8 changed files with 563 additions and 17 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.2.0"
version = "0.2.1"

[deps]

Expand Down
249 changes: 233 additions & 16 deletions src/ConfigEnv.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,238 @@ module ConfigEnv

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

export dotenv, dotenvx
export dotenv, dotenvx, isresolved, unresolved_keys

struct EnvProxyDict
struct EnvProxyDict{T}
dict::Dict{String, String}
env::T
undefined::Vector{String}
circular::Vector{String}
end
EnvProxyDict(dict) = EnvProxyDict(dict, ENV, String[], String[])
EnvProxyDict(dict, env) = EnvProxyDict(dict, env, String[], String[])

getindex(ed::EnvProxyDict, key) = get(ed.dict, key, ENV[key])
get(ed::EnvProxyDict, key, default) = get(ed.dict, key, get(ENV, key, default))
getindex(ed::EnvProxyDict, key) = get(ed.dict, key, ed.env[key])
get(ed::EnvProxyDict, key, default) = get(ed.dict, key, get(ed.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)
Base.:*(epd1::EnvProxyDict, epd2::EnvProxyDict...) = merge(epd1, epd2...; overwrite = false)
function imprint(epd::EnvProxyDict, env)
for (k, v) in epd.dict
env[k] = v
end
end

function merge(epd1::EnvProxyDict, epd2::EnvProxyDict...; overwrite = true)
epd = EnvProxyDict(foldl((x, y) -> merge(x, y.dict), epd2; init = epd1.dict), epd1.env)
resolve!(epd, epd.env)
overwrite && imprint(epd, epd.env)
epd
end

haskey(ed::EnvProxyDict, key) = haskey(ed.dict, key) || haskey(ed.env, key)

function merge!(epd1::EnvProxyDict, epd2::EnvProxyDict...)
function merge!(epd1::EnvProxyDict, epd2::EnvProxyDict...; overwrite = true)
foldl((x, y) -> merge!(x, y.dict), epd2; init = epd1.dict)
epd1
# You shouldn't use different envs during `merge!`. This is undefined behaviour.
# first dictionary environment can't be changed, cause it can be `ENV`
resolve!(epd1, epd1.env)
overwrite && imprint(epd1, epd1.env)
return epd1
end

function isdefin(s, i = 0, lev = 0)
i = nextind(s, i)
len = ncodeunits(s)
mode = 0
cnt_brackets = 0
while i <= len
c = s[i]
if c == '$'
mode = 1
elseif c == '{'
if mode == 1
return isdefin(s, i, 1)
else
cnt_brackets += 1
mode = 0
end
elseif c == '}'
mode = 0
cnt_brackets -= 1
lev == 1 && cnt_brackets < 0 && return false
else
mode = 0
end
i = nextind(s, i)
end

return true
end

mutable struct KVNode
key::String
value::String
children::Vector{KVNode}
parents_cnt::Int
isfinal::Bool
isresolved::Bool
end

KVNode(k, v) = KVNode(k, v, KVNode[], 0, true, false)
KVNode(v) = KVNode("", v, KVNode[], 0, false, false)
isresolved(kvnode::KVNode) = kvnode.isresolved
isresolved(epd::EnvProxyDict) = isempty(epd.undefined) && isempty(epd.circular)

function unresolved_keys(edp::EnvProxyDict)
undefined = map(k -> k => edp.dict[k], edp.undefined)
circular = map(k -> k => edp.dict[k], edp.circular)

return (; circular = circular, undefined = undefined)
end

function destructure!(v::KVNode, stack, knodes, i = 0, env = ENV)
val = v.value
len = ncodeunits(val)
i0 = nextind(val, i)
i = i0
bracket_count = 0
mode = 0 # everything is normal
isvalid = v.isfinal ? true : false

while i <= len
c = val[i]
if c == '$'
mode = 1 # waiting for the opening {
elseif c == '{'
if mode == 1
node = KVNode(val)
push!(node.children, v)
i, valid = destructure!(node, stack, knodes, i, env) # should return index of the closing bracket }
if valid
v.parents_cnt += 1
isvalid = true
end
else
bracket_count += 1
end
mode = 0 # calm down
elseif c == '}' # we should produce some reasonable result if this is our closing bracket
bracket_count -= 1
mode = 0
if !v.isfinal
bracket_count >= 0 && continue # false alarm
if v.parents_cnt == 0 # we are key node
v.key = v.value[i0:i-1]
if haskey(knodes, v.key)
append!(knodes[v.key].children, v.children)
empty!(v.children)
elseif haskey(env, v.key)
v.value = env[v.key]
push!(stack, v)
else
# Unknown key
v.value = v.value[i0-2:i]
push!(stack, v)
end
else # we are intermidiate node
v.value = v.value[i0:i-1]
end
return i, true
end
else
mode = 0
end

if i <= len
i = nextind(val, i)
end
end
return i, isvalid
end

function resolve!(edp::EnvProxyDict, env = ENV)
knodes = Dict{String, KVNode}()
for (k, v) in edp.dict
knodes[k] = KVNode(k, v)
end
stack = KVNode[]

prepare_stack!(stack, knodes, env)

resolve!(stack, knodes, env)

empty!(edp.undefined)
empty!(edp.circular)
for (k, v) in knodes
edp.dict[k] = v.value
if !v.isresolved
push!(edp.circular, k)
elseif !isdefin(v.value)
push!(edp.undefined, k)
end
end

edp
end

function prepare_stack!(stack, knodes, env)
for v in values(knodes)
# we extract interpolated terms from the value and last tier should be put on
# the stack for further imprinting
destructure!(v, stack, knodes, 0, env)
end

for v in values(knodes)
# if we do not have anything to interpolate inside the value, we are good.
# If we still have some other value where node should be used for interpolation,
# we put node on the interpolation stack
if v.parents_cnt == 0
v.isresolved = true
if !isempty(v.children)
push!(stack, v)
end
end
end
end

function recursive_replace!(node, key, val)
occursin(key, node.value) || return
node.value = replace(node.value, key => val)
foreach(x -> recursive_replace!(x, key, val), node.children)
nothing
end

Base.:*(epd1::EnvProxyDict, epd2::EnvProxyDict...) = EnvProxyDict(foldl((x, y) -> merge(x, y.dict), epd2; init = epd1.dict))
function resolve!(stack, knodes, env)
while !isempty(stack)
v = pop!(stack)
key = "\${" * v.key * "}"
while !isempty(v.children)
kid = pop!(v.children)
recursive_replace!(kid, key, v.value)
kid.parents_cnt -= 1
kid.parents_cnt == 0 || continue
if kid.isfinal
kid.isresolved = true
else
kid.key = kid.value
if haskey(knodes, kid.key)
append!(knodes[kid.key].children, kid.children)
empty!(kid.children)
if !isempty(knodes[kid.key].children) && knodes[kid.key].parents_cnt == 0
push!(stack, knodes[kid.key])
end
elseif haskey(env, kid.key)
kid.value = env[kid.key]
else
kid.value = "\${" * kid.key * "}"
end
end
if !isempty(kid.children)
push!(stack, kid)
end
end
end
end

"""
`ConfigEnv.parse` accepts a String or an IOBuffer (any value that
Expand Down Expand Up @@ -89,23 +303,26 @@ julia> cfg["USER"]
john_doe
```
"""
function dotenv(path = ".env"; overwrite = true)
function dotenv(path = ".env"; overwrite = true, env = ENV)
parsed = if (validatefile(path))
parse(read(path, String))
else
parse(path)
end

for (k, v) in parsed
if !haskey(ENV, k) || overwrite
ENV[k] = v
epd = EnvProxyDict(parsed, env)
resolve!(epd, env)

for (k, v) in epd.dict
if !haskey(env, k) || overwrite
env[k] = v
end
end

return EnvProxyDict(parsed)
return epd
end

dotenv(paths...; overwrite = true) = merge!(dotenv.(paths; overwrite = overwrite)...)
dotenv(paths...; overwrite = true, env = ENV) = merge!(dotenv.(paths; overwrite = overwrite, env = env)..., overwrite = overwrite)

"""
dotenvx(path1, path2, ...; overwrite = false)
Expand Down Expand Up @@ -138,6 +355,6 @@ julia> cfg["USER"]
john_doe
```
"""
dotenvx(paths...; overwrite = false) = dotenv(paths...; overwrite = overwrite)
dotenvx(paths...; overwrite = false, env = ENV) = dotenv(paths...; overwrite = overwrite, env = env)

end
2 changes: 2 additions & 0 deletions test/.env_interpolation1
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
X = A_${Y}
Y = FOO
3 changes: 3 additions & 0 deletions test/.env_interpolation2
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
USER = ${USER_${N}}
N = 1
USER_1 = FOO
2 changes: 2 additions & 0 deletions test/.env_interpolation3
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
USER = ${USER_${N}}
USER_1 = FOO
1 change: 1 addition & 0 deletions test/.env_interpolation4
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
N = 1
2 changes: 2 additions & 0 deletions test/.env_interpolation5
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
X = ${Y}
Y = ${X}
Loading

4 comments on commit 18dffd5

@Arkoniak
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Arkoniak
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/33258

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.2.1 -m "<description of version>" 18dffd528f67e9c1aca7dfa222f2829169754778
git push origin v0.2.1

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request updated: JuliaRegistries/General/33258

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.2.1 -m "<description of version>" 18dffd528f67e9c1aca7dfa222f2829169754778
git push origin v0.2.1

Please sign in to comment.