Repository: compose-spec/compose-go
Pull request #61 (2020-07-15 07:49:45)

Prefer ProjectOption over *ProjectOption for homogeneity
Make code homogeneous regarding references to `ProjectOption` (pointer vs value)

Introduce ProjectOptions constructor with functional parameters
This allows Compose implementations to use:
```
opts, err := NewProjectOptions( []string{ "mycompose.yaml"}, 
   WithOsEnv, WithDotEnv, WithEnv(variables), WithName("test") 
)
ProjectFromOptions( opts )
```


Patch:
diff --git a/cli/options.go b/cli/options.go
index baaa885..5380268 100644
--- a/cli/options.go
+++ b/cli/options.go
@@ -37,53 +37,84 @@ type ProjectOptions struct {
 	Name        string
 	WorkingDir  string
 	ConfigPaths []string
-	Environment []string
+	Environment map[string]string
 }
 
-// WithOsEnv imports environment variables from OS until those have been overridden by ProjectOptions.Environment
-func (o ProjectOptions) WithOsEnv() ProjectOptions {
-	env := getAsEqualsMap(o.Environment)
-	for k, v := range getAsEqualsMap(os.Environ()) {
-		if _, ok := env[k]; !ok {
-			env[k] = v
+type ProjectOptionsFn func(*ProjectOptions) error
+
+// NewProjectOptions creates ProjectOptions
+func NewProjectOptions(configs []string, opts ...ProjectOptionsFn) (*ProjectOptions, error) {
+	options := &ProjectOptions{
+		ConfigPaths: configs,
+		Environment: map[string]string{},
+	}
+	for _, o := range opts {
+		err := o(options)
+		if err != nil {
+			return nil, err
 		}
 	}
+	return options, nil
+}
 
-	return ProjectOptions{
-		Name:        o.Name,
-		WorkingDir:  o.WorkingDir,
-		ConfigPaths: o.ConfigPaths,
-		Environment: getAsStringList(env),
+// WithName defines ProjectOptions' name
+func WithName(name string) ProjectOptionsFn {
+	return func(o *ProjectOptions) error {
+		o.Name = name
+		return nil
 	}
 }
 
-// WithDotEnv imports environment variables from .env file until those have been overridden by ProjectOptions.Environment
-func (o ProjectOptions) WithDotEnv() (ProjectOptions, error) {
+// WithWorkingDirectory defines ProjectOptions' working directory
+func WithWorkingDirectory(wd string) ProjectOptionsFn {
+	return func(o *ProjectOptions) error {
+		o.WorkingDir = wd
+		return nil
+	}
+}
+
+// WithEnv defines a key=value set of variables used for compose file interpolation
+func WithEnv(env []string) ProjectOptionsFn {
+	return func(o *ProjectOptions) error {
+		for k, v := range getAsEqualsMap(env) {
+			o.Environment[k] = v
+		}
+		return nil
+	}
+}
+
+// WithOsEnv imports environment variables from OS
+func WithOsEnv(o *ProjectOptions) error {
+	for k, v := range getAsEqualsMap(os.Environ()) {
+		o.Environment[k] = v
+	}
+	return nil
+}
+
+// WithDotEnv imports environment variables from .env file
+func WithDotEnv(o *ProjectOptions) error {
 	dir, err := o.GetWorkingDir()
 	if err != nil {
-		return o, err
+		return err
 	}
 	dotEnvFile := filepath.Join(dir, ".env")
 	if _, err := os.Stat(dotEnvFile); os.IsNotExist(err) {
-		return o, nil
+		return nil
 	}
 	file, err := os.Open(dotEnvFile)
 	if err != nil {
-		return o, err
+		return err
 	}
 	defer file.Close()
 
 	env, err := godotenv.Parse(file)
 	if err != nil {
-		return o, err
+		return err
 	}
-
-	return ProjectOptions{
-		Name:        o.Name,
-		WorkingDir:  o.WorkingDir,
-		ConfigPaths: o.ConfigPaths,
-		Environment: getAsStringList(env),
-	}, nil
+	for k, v := range env {
+		o.Environment[k] = v
+	}
+	return nil
 }
 
 // DefaultFileNames defines the Compose file names for auto-discovery (in order of preference)
@@ -135,7 +166,7 @@ func ProjectFromOptions(options *ProjectOptions) (*types.Project, error) {
 	return loader.Load(types.ConfigDetails{
 		ConfigFiles: configs,
 		WorkingDir:  workingDir,
-		Environment: getAsEqualsMap(options.Environment),
+		Environment: options.Environment,
 	}, func(opts *loader.Options) {
 		if options.Name != "" {
 			opts.Name = options.Name


Test patch:
diff --git a/cli/options_test.go b/cli/options_test.go
index 9cc8aac..d83b16b 100644
--- a/cli/options_test.go
+++ b/cli/options_test.go
@@ -24,43 +24,40 @@ import (
 )
 
 func TestProjectName(t *testing.T) {
-	p, err := ProjectFromOptions(&ProjectOptions{
-		Name:        "my_project",
-		ConfigPaths: []string{"testdata/simple/compose.yaml"},
-	})
+	opts, err := NewProjectOptions([]string{"testdata/simple/compose.yaml"}, WithName("my_project"))
+	assert.NilError(t, err)
+	p, err := ProjectFromOptions(opts)
 	assert.NilError(t, err)
 	assert.Equal(t, p.Name, "my_project")
 
-	p, err = ProjectFromOptions(&ProjectOptions{
-		WorkingDir:  ".",
-		ConfigPaths: []string{"testdata/simple/compose.yaml"},
-	})
+	opts, err = NewProjectOptions([]string{"testdata/simple/compose.yaml"}, WithWorkingDirectory("."))
+	assert.NilError(t, err)
+	p, err = ProjectFromOptions(opts)
 	assert.NilError(t, err)
 	assert.Equal(t, p.Name, "cli")
 
-	p, err = ProjectFromOptions(&ProjectOptions{
-		ConfigPaths: []string{"testdata/simple/compose.yaml"},
-	})
+	opts, err = NewProjectOptions([]string{"testdata/simple/compose.yaml"})
+	assert.NilError(t, err)
+	p, err = ProjectFromOptions(opts)
 	assert.NilError(t, err)
 	assert.Equal(t, p.Name, "simple")
 
 	os.Setenv("COMPOSE_PROJECT_NAME", "my_project_from_env")
 	defer os.Unsetenv("COMPOSE_PROJECT_NAME")
-	p, err = ProjectFromOptions(&ProjectOptions{
-		ConfigPaths: []string{"testdata/simple/compose.yaml"},
-	})
+	opts, err = NewProjectOptions([]string{"testdata/simple/compose.yaml"}, WithOsEnv)
+	assert.NilError(t, err)
+	p, err = ProjectFromOptions(opts)
 	assert.NilError(t, err)
 	assert.Equal(t, p.Name, "my_project_from_env")
 }
 
 func TestProjectFromSetOfFiles(t *testing.T) {
-	p, err := ProjectFromOptions(&ProjectOptions{
-		Name: "my_project",
-		ConfigPaths: []string{
-			"testdata/simple/compose.yaml",
-			"testdata/simple/compose-with-overrides.yaml",
-		},
-	})
+	opts, err := NewProjectOptions([]string{
+		"testdata/simple/compose.yaml",
+		"testdata/simple/compose-with-overrides.yaml",
+	}, WithName("my_project"))
+	assert.NilError(t, err)
+	p, err := ProjectFromOptions(opts)
 	assert.NilError(t, err)
 	service, err := p.GetService("simple")
 	assert.NilError(t, err)
@@ -68,14 +65,11 @@ func TestProjectFromSetOfFiles(t *testing.T) {
 }
 
 func TestProjectWithDotEnv(t *testing.T) {
-	options, err := ProjectOptions{
-		Name: "my_project",
-		ConfigPaths: []string{
-			"testdata/simple/compose-with-variables.yaml",
-		},
-	}.WithDotEnv()
+	opts, err := NewProjectOptions([]string{
+		"testdata/simple/compose-with-variables.yaml",
+	}, WithName("my_project"), WithDotEnv)
 	assert.NilError(t, err)
-	p, err := ProjectFromOptions(&options)
+	p, err := ProjectFromOptions(opts)
 	assert.NilError(t, err)
 	service, err := p.GetService("simple")
 	assert.NilError(t, err)