diff --git a/README.md b/README.md index 64a15bf..1140455 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,39 @@ # dot_env + + +- [dot_env](#dotenv) + - [Quick start](#quick-start) + - [Installation](#installation) + + [![Package Version](https://img.shields.io/hexpm/v/dot_env)](https://hex.pm/packages/dotenv) [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/dot_env/) -dot_env is a port of the popular JavaScript [dotenv](https://github.com/motdotla/dotenv/blob/master/lib/main.js) package that helps you load environment variables from .env (or other) files. +dot_env is a port of the popular JavaScript [dotenv](https://github.com/motdotla/dotenv) package that helps you load environment variables from .env (or other) files. -> This package may support other formats in the future but for now, only supports the popular .env format +> This package may support other formats in the future but for now, supports the popular .env format +> +> You can find the Javascript test [here](https://github.com/aosasona/dot_js_test) ## Quick start -```sh -gleam run # Run the project -gleam test # Run the tests -gleam shell # Run an Erlang shell +```gleam +import dot_env +import dot_env/env +import gleam/io + +pub fn main() { + dot_env.load_with_opts(dot_env.Opts(path: "path/to/.env", debug: False, capitalize: False)) + // or `dot_env.load()` to load it the `.env` file in the root path + + case env.get("MY_ENV_VAR") { + Ok(value) -> io.println(value) + Error(_) -> io.println("something went wrong") + } + + Nil +} ``` ## Installation diff --git a/gleam.toml b/gleam.toml index 381a840..e014040 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,23 +1,19 @@ name = "dot_env" -version = "0.2.0" +version = "0.2.1" -# Fill out these fields if you intend to generate HTML documentation or publish -# your project to the Hex package manager. -# -description = "Load environment variables from a .env file" +description = "Load environment variables from files" licences = ["Apache-2.0"] repository = { type = "github", user = "aosasona", repo = "dotenv" } -# links = [{ title = "Website", href = "https://gleam.run" }] internal_modules = [ "dot_env/internal/*" ] + gleam = ">= 0.32.0" [dependencies] gleam_stdlib = "~> 0.32" simplifile = "~> 0.1" -gleam_erlang = "~> 0.22" [dev-dependencies] gleeunit = "~> 0.10" diff --git a/manifest.toml b/manifest.toml index b2a6406..549a889 100644 --- a/manifest.toml +++ b/manifest.toml @@ -2,14 +2,12 @@ # You typically do not need to edit this file packages = [ - { name = "gleam_erlang", version = "0.22.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "367D8B41A7A86809928ED1E7E55BFD0D46D7C4CF473440190F324AFA347109B4" }, { name = "gleam_stdlib", version = "0.32.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "07D64C26D014CF570F8ACADCE602761EA2E74C842D26F2FD49B0D61973D9966F" }, { name = "gleeunit", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "1397E5C4AC4108769EE979939AC39BF7870659C5AFB714630DEEEE16B8272AD5" }, { name = "simplifile", version = "0.1.14", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "10EA0207796F20488A3A166C50A189C9385333F3C9FAC187729DE7B9CE4ADDBC" }, ] [requirements] -gleam_erlang = { version = "~> 0.22" } gleam_stdlib = { version = "~> 0.32" } gleeunit = { version = "~> 0.10" } simplifile = { version = "~> 0.1" } diff --git a/src/dot_env.gleam b/src/dot_env.gleam index 00e010d..091d701 100644 --- a/src/dot_env.gleam +++ b/src/dot_env.gleam @@ -1,8 +1,8 @@ import gleam/io import gleam/string -import gleam/erlang/os import gleam/result.{try} import dot_env/internal/parser +import dot_env/env import simplifile pub type Opts { @@ -94,26 +94,26 @@ fn recursively_set_environment_variables( kv_pairs: parser.EnvPairs, ) { case kv_pairs { - [pair, ..rest] -> { - os.set_env( + [] -> Nil + [pair] -> { + env.set( case config.capitalize { True -> string.uppercase(pair.0) False -> pair.0 }, pair.1, ) - recursively_set_environment_variables(config, rest) } - [pair] -> { - os.set_env( + [pair, ..rest] -> { + env.set( case config.capitalize { True -> string.uppercase(pair.0) False -> pair.0 }, pair.1, ) + recursively_set_environment_variables(config, rest) } - [] -> Nil } } diff --git a/src/dot_env/env.gleam b/src/dot_env/env.gleam new file mode 100644 index 0000000..4e531c4 --- /dev/null +++ b/src/dot_env/env.gleam @@ -0,0 +1,28 @@ +/// Set an environment variable (supports both Erlang and JavaScript targets) +/// +/// Example: +/// ```gleam +/// import dot_env/env +/// +/// env.set("MY_ENV_VAR", "my value") +/// ``` +/// +@external(erlang, "dot_env_ffi", "set_env") +@external(javascript, "../dot_env_ffi.mjs", "set_env") +pub fn set(key: String, value: String) -> Nil + +/// Get an environment variable (supports both Erlang and JavaScript targets) +/// +/// Example: +/// ```gleam +/// import dot_env/env +/// import gleam/io +/// import gleam/result +/// +/// env.get("MY_ENV_VAR") +/// |> result.unwrap("NOT SET") +/// |> io.println +/// ``` +@external(erlang, "dot_env_ffi", "get_env") +@external(javascript, "../dot_env_ffi.mjs", "get_env") +pub fn get(key: String) -> Result(String, String) diff --git a/src/dot_env/internal/parser.gleam b/src/dot_env/internal/parser.gleam index d369478..b958309 100644 --- a/src/dot_env/internal/parser.gleam +++ b/src/dot_env/internal/parser.gleam @@ -1,5 +1,4 @@ import gleam/bool -import gleam/io import gleam/string import gleam/list import gleam/option.{Some} diff --git a/src/dot_env_ffi.erl b/src/dot_env_ffi.erl new file mode 100644 index 0000000..fd8e8a0 --- /dev/null +++ b/src/dot_env_ffi.erl @@ -0,0 +1,15 @@ +-module(dot_env_ffi). + +-export([get_env/1, set_env/2]). + +get_env(Name) -> + case os:getenv(binary_to_list(Name)) of + false -> + {error, nil}; + Value -> + {ok, list_to_binary(Value)} + end. + +set_env(Name, Value) -> + os:putenv(binary_to_list(Name), binary_to_list(Value)), + nil. diff --git a/src/dot_env_ffi.mjs b/src/dot_env_ffi.mjs new file mode 100644 index 0000000..2ad4c00 --- /dev/null +++ b/src/dot_env_ffi.mjs @@ -0,0 +1,27 @@ +import { Ok as GleamOk, Error as GleamError } from "./gleam.mjs"; + +const Nil = undefined; + +export function set_env(key, value) { + if (!process.env) { + console.error("process.env is not available"); + return Nil; + } + + process.env[key] = value; + return Nil; +} + +export function get_env(key) { + if (!process.env) { + console.error("process.env is not available"); + return new GleamError("process.env is not available"); + } + + const value = process.env[key]; + if (!value) { + return new GleamError(`key \`${key}\` is not set`); + } + + return new GleamOk(value); +} diff --git a/test/dot_env_test.gleam b/test/dot_env_test.gleam index 17bb55b..bbacde3 100644 --- a/test/dot_env_test.gleam +++ b/test/dot_env_test.gleam @@ -1,5 +1,5 @@ import dot_env.{Opts} -import gleam/erlang/os +import dot_env/env import gleeunit import gleeunit/should @@ -10,19 +10,19 @@ pub fn main() { pub fn load_default_test() { dot_env.load() - os.get_env("PORT") + env.get("PORT") |> should.equal(Ok("9000")) - os.get_env("APP_NAME") + env.get("APP_NAME") |> should.equal(Ok("app")) - os.get_env("APP_ENV") + env.get("APP_ENV") |> should.equal(Ok("local")) - os.get_env("APP_KEY") + env.get("APP_KEY") |> should.equal(Ok("base-64:0")) - os.get_env("APP_DEBUG") + env.get("APP_DEBUG") |> should.equal(Ok("true")) } @@ -33,102 +33,102 @@ pub fn load_normal_test() { capitalize: True, )) - os.get_env("BASIC") + env.get("BASIC") |> should.equal(Ok("basic")) - os.get_env("AFTER_LINE") + env.get("AFTER_LINE") |> should.equal(Ok("after_line")) - os.get_env("EMPTY") + env.get("EMPTY") |> should.equal(Ok("")) - os.get_env("EMPTY_SINGLE_QUOTES") + env.get("EMPTY_SINGLE_QUOTES") |> should.equal(Ok("")) - os.get_env("EMPTY_DOUBLE_QUOTES") + env.get("EMPTY_DOUBLE_QUOTES") |> should.equal(Ok("")) - os.get_env("SINGLE_QUOTES") + env.get("SINGLE_QUOTES") |> should.equal(Ok("single_quotes")) - os.get_env("SINGLE_QUOTES_SPACED") + env.get("SINGLE_QUOTES_SPACED") |> should.equal(Ok(" single quotes ")) - os.get_env("DOUBLE_QUOTES_INSIDE_SINGLE") + env.get("DOUBLE_QUOTES_INSIDE_SINGLE") |> should.equal(Ok("double \"quotes\" work inside single quotes")) - os.get_env("DOUBLE_QUOTES_WITH_NO_SPACE_BRACKET") + env.get("DOUBLE_QUOTES_WITH_NO_SPACE_BRACKET") |> should.equal(Ok("{ port: $MONGOLAB_PORT}")) - os.get_env("SINGLE_QUOTES_INSIDE_DOUBLE") + env.get("SINGLE_QUOTES_INSIDE_DOUBLE") |> should.equal(Ok("single 'quotes' work inside double quotes")) - os.get_env("BACKTICKS_INSIDE_SINGLE") + env.get("BACKTICKS_INSIDE_SINGLE") |> should.equal(Ok("`backticks` work inside single quotes")) - os.get_env("BACKTICKS_INSIDE_DOUBLE") + env.get("BACKTICKS_INSIDE_DOUBLE") |> should.equal(Ok("`backticks` work inside double quotes")) - os.get_env("BACKTICKS") + env.get("BACKTICKS") |> should.equal(Ok("backticks")) - os.get_env("BACKTICKS_SPACED") + env.get("BACKTICKS_SPACED") |> should.equal(Ok(" backticks ")) - os.get_env("DOUBLE_QUOTES_INSIDE_BACKTICKS") + env.get("DOUBLE_QUOTES_INSIDE_BACKTICKS") |> should.equal(Ok("double \"quotes\" work inside backticks")) - os.get_env("SINGLE_QUOTES_INSIDE_BACKTICKS") + env.get("SINGLE_QUOTES_INSIDE_BACKTICKS") |> should.equal(Ok("single 'quotes' work inside backticks")) - os.get_env("DOUBLE_AND_SINGLE_QUOTES_INSIDE_BACKTICKS") + env.get("DOUBLE_AND_SINGLE_QUOTES_INSIDE_BACKTICKS") |> should.equal(Ok( "double \"quotes\" and single 'quotes' work inside backticks", )) - os.get_env("EXPAND_NEWLINES") + env.get("EXPAND_NEWLINES") |> should.equal(Ok("expand\nnew\nlines")) - os.get_env("DONT_EXPAND_UNQUOTED") + env.get("DONT_EXPAND_UNQUOTED") |> should.equal(Ok("dontexpand\\nnewlines")) - os.get_env("DONT_EXPAND_SQUOTED") + env.get("DONT_EXPAND_SQUOTED") |> should.equal(Ok("dontexpand\\nnewlines")) - os.get_env("INLINE_COMMENTS") + env.get("INLINE_COMMENTS") |> should.equal(Ok("inline comments")) - os.get_env("INLINE_COMMENTS_SINGLE_QUOTES") + env.get("INLINE_COMMENTS_SINGLE_QUOTES") |> should.equal(Ok("inline comments outside of #singlequotes")) - os.get_env("INLINE_COMMENTS_DOUBLE_QUOTES") + env.get("INLINE_COMMENTS_DOUBLE_QUOTES") |> should.equal(Ok("inline comments outside of #doublequotes")) - os.get_env("INLINE_COMMENTS_BACKTICKS") + env.get("INLINE_COMMENTS_BACKTICKS") |> should.equal(Ok("inline comments outside of #backticks")) - os.get_env("INLINE_COMMENTS_SPACE") + env.get("INLINE_COMMENTS_SPACE") |> should.equal(Ok("inline comments start with a")) - os.get_env("EQUAL_SIGNS") + env.get("EQUAL_SIGNS") |> should.equal(Ok("equals==")) - os.get_env("RETAIN_INNER_QUOTES") + env.get("RETAIN_INNER_QUOTES") |> should.equal(Ok("{\"foo\": \"bar\"}")) - os.get_env("RETAIN_INNER_QUOTES_AS_STRING") + env.get("RETAIN_INNER_QUOTES_AS_STRING") |> should.equal(Ok("{\"foo\": \"bar\"}")) - os.get_env("RETAIN_INNER_QUOTES_AS_BACKTICKS") + env.get("RETAIN_INNER_QUOTES_AS_BACKTICKS") |> should.equal(Ok("{\"foo\": \"bar's\"}")) - os.get_env("TRIM_SPACE_FROM_UNQUOTED") + env.get("TRIM_SPACE_FROM_UNQUOTED") |> should.equal(Ok("some spaced out string")) - os.get_env("USERNAME") + env.get("USERNAME") |> should.equal(Ok("therealnerdybeast@example.tld")) - os.get_env("SPACED_KEY") + env.get("SPACED_KEY") |> should.equal(Ok("parsed")) } @@ -139,60 +139,60 @@ pub fn load_multiline_test() { capitalize: True, )) - os.get_env("BASIC") + env.get("BASIC") |> should.equal(Ok("basic")) - os.get_env("AFTER_LINE") + env.get("AFTER_LINE") |> should.equal(Ok("after_line")) - os.get_env("EMPTY") + env.get("EMPTY") |> should.equal(Ok("")) - os.get_env("SINGLE_QUOTES") + env.get("SINGLE_QUOTES") |> should.equal(Ok("single_quotes")) - os.get_env("SINGLE_QUOTES_SPACED") + env.get("SINGLE_QUOTES_SPACED") |> should.equal(Ok(" single quotes ")) - os.get_env("DOUBLE_QUOTES") + env.get("DOUBLE_QUOTES") |> should.equal(Ok("double_quotes")) - os.get_env("DOUBLE_QUOTES_SPACED") + env.get("DOUBLE_QUOTES_SPACED") |> should.equal(Ok(" double quotes ")) - os.get_env("EXPAND_NEWLINES") + env.get("EXPAND_NEWLINES") |> should.equal(Ok("expand\nnew\nlines")) - os.get_env("DONT_EXPAND_UNQUOTED") + env.get("DONT_EXPAND_UNQUOTED") |> should.equal(Ok("dontexpand\\nnewlines")) - os.get_env("DONT_EXPAND_SQUOTED") + env.get("DONT_EXPAND_SQUOTED") |> should.equal(Ok("dontexpand\\nnewlines")) - os.get_env("EQUAL_SIGNS") + env.get("EQUAL_SIGNS") |> should.equal(Ok("equals==")) - os.get_env("RETAIN_INNER_QUOTES") + env.get("RETAIN_INNER_QUOTES") |> should.equal(Ok("{\"foo\": \"bar\"}")) - os.get_env("RETAIN_INNER_QUOTES_AS_STRING") + env.get("RETAIN_INNER_QUOTES_AS_STRING") |> should.equal(Ok("{\"foo\": \"bar\"}")) - os.get_env("TRIM_SPACE_FROM_UNQUOTED") + env.get("TRIM_SPACE_FROM_UNQUOTED") |> should.equal(Ok("some spaced out string")) - os.get_env("USERNAME") + env.get("USERNAME") |> should.equal(Ok("therealnerdybeast@example.tld")) - os.get_env("SPACED_KEY") + env.get("SPACED_KEY") |> should.equal(Ok("parsed")) - os.get_env("MULTI_DOUBLE_QUOTED") + env.get("MULTI_DOUBLE_QUOTED") |> should.equal(Ok("THIS\nIS\nA\nMULTILINE\nSTRING")) - // os.get_env("MULTI_SINGLE_QUOTED") + // env.get("MULTI_SINGLE_QUOTED") // |> should.equal(Ok("THIS\nIS\nA\nMULTILINE\nSTRING")) - os.get_env("MULTI_BACKTICKED") + env.get("MULTI_BACKTICKED") |> should.equal(Ok("THIS\nIS\nA\n\"MULTILINE'S\"\nSTRING")) }