Skip to content

Commit

Permalink
Add env package for accessing environment variables; new Map methods …
Browse files Browse the repository at this point in the history
…in opt package
  • Loading branch information
jpfourny committed Feb 7, 2024
1 parent fc870f4 commit 9cfd78d
Show file tree
Hide file tree
Showing 5 changed files with 442 additions and 47 deletions.
107 changes: 107 additions & 0 deletions pkg/env/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package env

import (
"github.com/jpfourny/papaya/pkg/constraint"
"github.com/jpfourny/papaya/pkg/opt"
"github.com/jpfourny/papaya/pkg/pair"
"github.com/jpfourny/papaya/pkg/stream"
"github.com/jpfourny/papaya/pkg/stream/mapper"
"os"
"strings"
)

// Set sets the environment variable with the given key to the given value.
func Set(key, value string) {
_ = os.Setenv(key, value)
}

// Unset unsets the environment variable with the given key.
func Unset(key string) {
_ = os.Unsetenv(key)
}

// SetAllPairs sets the environment variables to the given pairs.
// Returns a function that can be called to revert the changes.
func SetAllPairs(pairs ...pair.Pair[string, string]) (revert func()) {
return setAll(stream.Of(pairs...))
}

// SetAllMap sets the environment variables to the given map.
// Returns a function that can be called to revert the changes.
func SetAllMap(m map[string]string) (revert func()) {
return setAll(stream.FromMap(m))
}

func setAll(vars stream.Stream[pair.Pair[string, string]]) (revert func()) {
backupVars := make(map[string]string)
var addedKeys []string

stream.ForEach(vars, func(p pair.Pair[string, string]) {
if prev, ok := os.LookupEnv(p.First()); ok {
backupVars[p.First()] = prev // Backup existing key before overwriting.
} else {
addedKeys = append(addedKeys, p.First()) // Keep track of added keys.
}
Set(p.First(), p.Second())
})

return func() {
for k, v := range backupVars {
Set(k, v)
}
for _, k := range addedKeys {
Unset(k)
}
}
}

// Get returns the value of the environment variable with the given key, if it exists.
func Get(key string) opt.Optional[string] {
return opt.Maybe(os.LookupEnv(key))
}

// GetBool returns the value of the environment variable with the given key, if it exists and can be parsed as a boolean.
// An empty Optional is returned if the variable is unset or if value cannot be parsed as a boolean.
func GetBool(key string) opt.Optional[bool] {
return opt.OptionalMap(
Get(key),
mapper.TryParseBool[string](),
)
}

func GetInt[I constraint.SignedInteger](key string) opt.Optional[I] {
return opt.OptionalMap(
Get(key),
mapper.TryParseInt[string, I](10, 64),
)
}

func GetUInt[I constraint.UnsignedInteger](key string) opt.Optional[I] {
return opt.OptionalMap(
Get(key),
mapper.TryParseUint[string, I](10, 64),
)
}

func GetFloat[F constraint.Float](key string) opt.Optional[F] {
return opt.OptionalMap(
Get(key),
mapper.TryParseFloat[string, F](64),
)
}

// ToStream returns a stream of pairs representing the environment variables.
func ToStream() stream.Stream[pair.Pair[string, string]] {
return stream.Map(
stream.FromSlice(os.Environ()),
func(s string) pair.Pair[string, string] {
parts := strings.SplitN(s, "=", 2)
return pair.Of(parts[0], parts[1])
},
)
}

// ToMap returns a map representing the environment variables.
func ToMap() map[string]string {
return stream.CollectMap(ToStream())
}
209 changes: 209 additions & 0 deletions pkg/env/env_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package env

import (
"github.com/jpfourny/papaya/pkg/pair"
"github.com/jpfourny/papaya/pkg/stream"
"os"
"testing"
)

func TestSet(t *testing.T) {
Set("foo", "bar")

if os.Getenv("foo") != "bar" {
t.Errorf("expected os.Getenv(%q) to return %q; got %q", "foo", "bar", os.Getenv("foo"))
}
}

func TestUnset(t *testing.T) {
_ = os.Setenv("foo", "bar")
Unset("foo")

if os.Getenv("foo") != "" {
t.Errorf("expected os.Getenv(%q) to return %q; got %q", "foo", "", os.Getenv("foo"))
}
}

func TestSetAllPairs(t *testing.T) {
_ = os.Setenv("foo", "bar")

undo := SetAllPairs(
pair.Of("foo", "bar2"),
pair.Of("baz", "qux"),
)

if os.Getenv("foo") != "bar2" {
t.Errorf("expected os.Getenv(%q) to return %q; got %q", "foo", "bar2", os.Getenv("foo"))
}
if os.Getenv("baz") != "qux" {
t.Errorf("expected os.Getenv(%q) to return %q; got %q", "baz", "qux", os.Getenv("baz"))
}

undo()

if os.Getenv("foo") != "bar" {
t.Errorf("expected os.Getenv(%q) to return %q; got %q", "foo", "bar", os.Getenv("foo"))
}
if os.Getenv("baz") != "" {
t.Errorf("expected os.Getenv(%q) to return %q; got %q", "baz", "", os.Getenv("baz"))
}
}

func TestSetAllMap(t *testing.T) {
_ = os.Setenv("foo", "bar")

undo := SetAllMap(map[string]string{
"foo": "bar2",
"baz": "qux",
})

if os.Getenv("foo") != "bar2" {
t.Errorf("expected os.Getenv(%q) to return %q; got %q", "foo", "bar2", os.Getenv("foo"))
}
if os.Getenv("baz") != "qux" {
t.Errorf("expected os.Getenv(%q) to return %q; got %q", "baz", "qux", os.Getenv("baz"))
}

undo()

if os.Getenv("foo") != "bar" {
t.Errorf("expected os.Getenv(%q) to return %q; got %q", "foo", "bar", os.Getenv("foo"))
}
if os.Getenv("baz") != "" {
t.Errorf("expected os.Getenv(%q) to return %q; got %q", "baz", "", os.Getenv("baz"))
}
}

func TestGet(t *testing.T) {
_ = os.Setenv("foo", "bar")

got := Get("foo")
if !got.Present() || got.GetOrZero() != "bar" {
t.Errorf("expected Get(%q) to return %q; got %v", "foo", "bar", got)
}

got = Get("baz")
if got.Present() {
t.Errorf("expected Get(%q) to return empty opt; got %v", "baz", got)
}
}

func TestGetBool(t *testing.T) {
_ = os.Setenv("foo", "true")

got := GetBool("foo")
if !got.Present() || !got.GetOrZero() {
t.Errorf("expected GetBool(%q) to return true; got %v", "foo", got)
}

_ = os.Setenv("foo", "false")

got = GetBool("foo")
if !got.Present() || got.GetOrZero() {
t.Errorf("expected GetBool(%q) to return false; got %v", "foo", got)
}

got = GetBool("baz")
if got.Present() {
t.Errorf("expected GetBool(%q) to return empty opt; got %v", "baz", got)
}

_ = os.Setenv("foo", "not-a-bool")

got = GetBool("foo")
if got.Present() {
t.Errorf("expected GetBool(%q) to return empty opt; got %v", "foo", got)
}
}

func TestGetInt(t *testing.T) {
_ = os.Setenv("foo", "42")

got := GetInt[int]("foo")
if !got.Present() || got.GetOrZero() != 42 {
t.Errorf("expected GetInt(%q) to return 42; got %v", "foo", got)
}

got = GetInt[int]("baz")
if got.Present() {
t.Errorf("expected GetInt(%q) to return empty opt; got %v", "baz", got)
}

_ = os.Setenv("foo", "not-an-int")

got = GetInt[int]("foo")
if got.Present() {
t.Errorf("expected GetInt(%q) to return empty opt; got %v", "foo", got)
}
}

func TestGetUInt(t *testing.T) {
_ = os.Setenv("foo", "42")

got := GetUInt[uint]("foo")
if !got.Present() || got.GetOrZero() != 42 {
t.Errorf("expected GetUInt(%q) to return 42; got %v", "foo", got)
}

got = GetUInt[uint]("baz")
if got.Present() {
t.Errorf("expected GetUInt(%q) to return empty opt; got %v", "baz", got)
}

_ = os.Setenv("foo", "not-an-uint")

got = GetUInt[uint]("foo")
if got.Present() {
t.Errorf("expected GetUInt(%q) to return empty opt; got %v", "foo", got)
}
}

func TestGetFloat(t *testing.T) {
_ = os.Setenv("foo", "42.42")

got := GetFloat[float64]("foo")
if !got.Present() || got.GetOrZero() != 42.42 {
t.Errorf("expected GetFloat(%q) to return 42.42; got %v", "foo", got)
}

got = GetFloat[float64]("baz")
if got.Present() {
t.Errorf("expected GetFloat(%q) to return empty opt; got %v", "baz", got)
}

_ = os.Setenv("foo", "not-a-float")

got = GetFloat[float64]("foo")
if got.Present() {
t.Errorf("expected GetFloat(%q) to return empty opt; got %v", "foo", got)
}
}

func TestToStream(t *testing.T) {
_ = os.Setenv("foo", "bar")
_ = os.Setenv("baz", "qux")

got := ToStream()
m := stream.CollectMap(got)

if m["foo"] != "bar" {
t.Errorf("expected ToStream() to contain %q; got %v", "foo", m["foo"])
}
if m["baz"] != "qux" {
t.Errorf("expected ToStream() to contain %q; got %v", "baz", m["baz"])
}
}

func TestToMap(t *testing.T) {
_ = os.Setenv("foo", "bar")
_ = os.Setenv("baz", "qux")

got := ToMap()

if got["foo"] != "bar" {
t.Errorf("expected ToMap() to contain %q; got %v", "foo", got["foo"])
}
if got["baz"] != "qux" {
t.Errorf("expected ToMap() to contain %q; got %v", "baz", got["baz"])
}
}
56 changes: 54 additions & 2 deletions pkg/opt/optional.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ func Any[V any](options ...Optional[V]) Optional[V] {
return None[V]{}
}

// Map returns an Optional containing the result of applying the provided function to the value contained in the provided Optional.
// If the provided Optional is empty, an empty Optional is returned.
// Map returns an Optional containing the result of applying the provided mapper function to the value contained in the provided Optional.
// If the provided Optional is empty, an empty Optional is returned; otherwise, a non-empty Optional is returned.
//
// Example usage:
//
Expand All @@ -52,6 +52,58 @@ func Map[V, U any](o Optional[V], mapper func(V) U) Optional[U] {
return Empty[U]()
}

// OptionalMap returns an Optional containing the result of applying the provided mapper function to the value contained in the provided Optional.
// If the provided Optional is empty, of if the mapper returns an empty Optional, an empty Optional is returned; otherwise, a non-empty Optional is returned.
//
// Example usage:
//
// o := opt.OptionalMap(
// opt.Of(1),
// func(i int) opt.Optional[string] { return opt.Of(fmt.Sprintf("%d", i)) },
// ) // opt.Some("1")
//
// o = opt.OptionalMap(
// opt.Of(1),
// func(i int) opt.Optional[string] { return opt.Empty[string]() },
// ) // opt.None()
//
// o = opt.OptionalMap(
// opt.Empty[int](),
// func(i int) opt.Optional[string] { return opt.Of(fmt.Sprintf("%d", i)) },
// ) // opt.None()
func OptionalMap[V, U any](o Optional[V], mapper func(V) Optional[U]) Optional[U] {
if value, ok := o.Get(); ok {
return mapper(value)
}
return Empty[U]()
}

// MaybeMap returns an Optional containing the result of applying the provided mapper function to the value contained in the provided Optional.
// If the provided Optional is empty, or if the mapper returns false, an empty Optional is returned; otherwise, a non-empty Optional is returned.
//
// Example usage:
//
// o := opt.MaybeMap(
// opt.Of(1),
// func(i int) (string, bool) { return fmt.Sprintf("%d", i), true },
// ) // opt.Some("1")
//
// o = opt.MaybeMap(
// opt.Of(1),
// func(i int) (string, bool) { return fmt.Sprintf("%d", i), false },
// ) // opt.None()
//
// o = opt.MaybeMap(
// opt.Empty[int](),
// func(i int) (string, bool) { return fmt.Sprintf("%d", i), true },
// ) // opt.None()
func MaybeMap[V, U any](o Optional[V], mapper func(V) (U, bool)) Optional[U] {
if value, ok := o.Get(); ok {
return Maybe(mapper(value))
}
return Empty[U]()
}

// Optional is a generic type that takes one type parameter V and represents a value that may or may not be present.
// It is similar to Java's Optional type.
type Optional[V any] interface {
Expand Down
Loading

0 comments on commit 9cfd78d

Please sign in to comment.