Repository: compose-spec/compose-go
Pull request #574 (2024-02-14 07:47:17)

pass context as first parameter
As compose/v2 introduces a few breaking changes, I suggest we also remove `Options.WithContext` and prefer use of `ctx` passed as first func argument, applying recommended go-idom


Patch:
diff --git a/cli/options.go b/cli/options.go
index 1e86753..d2c91fb 100644
--- a/cli/options.go
+++ b/cli/options.go
@@ -37,8 +37,6 @@ import (
 
 // ProjectOptions provides common configuration for loading a project.
 type ProjectOptions struct {
-	ctx context.Context
-
 	// Name is a valid Compose project name to be used or empty.
 	//
 	// If empty, the project loader will automatically infer a reasonable
@@ -339,14 +337,6 @@ func WithResolvedPaths(resolve bool) ProjectOptionsFn {
 	}
 }
 
-// WithContext sets the context used to load model and resources
-func WithContext(ctx context.Context) ProjectOptionsFn {
-	return func(o *ProjectOptions) error {
-		o.ctx = ctx
-		return nil
-	}
-}
-
 // WithResourceLoader register support for ResourceLoader to manage remote resources
 func WithResourceLoader(r loader.ResourceLoader) ProjectOptionsFn {
 	return func(o *ProjectOptions) error {
@@ -391,7 +381,7 @@ var DefaultOverrideFileNames = []string{"compose.override.yml", "compose.overrid
 
 func (o ProjectOptions) GetWorkingDir() (string, error) {
 	if o.WorkingDir != "" {
-		return o.WorkingDir, nil
+		return filepath.Abs(o.WorkingDir)
 	}
 	for _, path := range o.ConfigPaths {
 		if path != "-" {
@@ -405,9 +395,8 @@ func (o ProjectOptions) GetWorkingDir() (string, error) {
 	return os.Getwd()
 }
 
-// ProjectFromOptions load a compose project based on command line options
-func ProjectFromOptions(options *ProjectOptions) (*types.Project, error) {
-	configPaths, err := getConfigPathsFromOptions(options)
+func (o ProjectOptions) GeConfigFiles() ([]types.ConfigFile, error) {
+	configPaths, err := o.getConfigPaths()
 	if err != nil {
 		return nil, err
 	}
@@ -435,26 +424,26 @@ func ProjectFromOptions(options *ProjectOptions) (*types.Project, error) {
 			Content:  b,
 		})
 	}
+	return configs, err
+}
 
-	workingDir, err := options.GetWorkingDir()
+// ProjectFromOptions load a compose project based on command line options
+func ProjectFromOptions(ctx context.Context, options *ProjectOptions) (*types.Project, error) {
+	configs, err := options.GeConfigFiles()
 	if err != nil {
 		return nil, err
 	}
-	absWorkingDir, err := filepath.Abs(workingDir)
+
+	workingDir, err := options.GetWorkingDir()
 	if err != nil {
 		return nil, err
 	}
 
 	options.loadOptions = append(options.loadOptions,
-		withNamePrecedenceLoad(absWorkingDir, options),
+		withNamePrecedenceLoad(workingDir, options),
 		withConvertWindowsPaths(options),
 		withListener(options))
 
-	ctx := options.ctx
-	if ctx == nil {
-		ctx = context.Background()
-	}
-
 	project, err := loader.LoadWithContext(ctx, types.ConfigDetails{
 		ConfigFiles: configs,
 		WorkingDir:  workingDir,
@@ -464,7 +453,10 @@ func ProjectFromOptions(options *ProjectOptions) (*types.Project, error) {
 		return nil, err
 	}
 
-	project.ComposeFiles = configPaths
+	for _, config := range configs {
+		project.ComposeFiles = append(project.ComposeFiles, config.Filename)
+	}
+
 	return project, nil
 }
 
@@ -498,10 +490,10 @@ func withListener(options *ProjectOptions) func(*loader.Options) {
 	}
 }
 
-// getConfigPathsFromOptions retrieves the config files for project based on project options
-func getConfigPathsFromOptions(options *ProjectOptions) ([]string, error) {
-	if len(options.ConfigPaths) != 0 {
-		return absolutePaths(options.ConfigPaths)
+// getConfigPaths retrieves the config files for project based on project options
+func (o *ProjectOptions) getConfigPaths() ([]string, error) {
+	if len(o.ConfigPaths) != 0 {
+		return absolutePaths(o.ConfigPaths)
 	}
 	return nil, fmt.Errorf("no configuration file provided: %w", errdefs.ErrNotFound)
 }
diff --git a/cmd/main.go b/cmd/main.go
index 2a25580..7c117b0 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -17,6 +17,7 @@
 package main
 
 import (
+	"context"
 	"flag"
 	"fmt"
 	"os"
@@ -60,7 +61,7 @@ Usage: compose-spec [OPTIONS] COMPOSE_FILE [COMPOSE_OVERRIDE_FILE]`)
 		exitError("failed to configure project options", err)
 	}
 
-	project, err := cli.ProjectFromOptions(options)
+	project, err := cli.ProjectFromOptions(context.Background(), options)
 	if err != nil {
 		exitError("failed to load project", err)
 	}


Test patch:
diff --git a/cli/options_test.go b/cli/options_test.go
index 07565c3..31edf9b 100644
--- a/cli/options_test.go
+++ b/cli/options_test.go
@@ -17,6 +17,7 @@
 package cli
 
 import (
+	"context"
 	"fmt"
 	"os"
 	"path/filepath"
@@ -33,7 +34,7 @@ func TestProjectName(t *testing.T) {
 	t.Run("by name", func(t *testing.T) {
 		opts, err := NewProjectOptions([]string{"testdata/simple/compose.yaml"}, WithName("my_project"))
 		assert.NilError(t, err)
-		p, err := ProjectFromOptions(opts)
+		p, err := ProjectFromOptions(context.TODO(), opts)
 		assert.NilError(t, err)
 		assert.Equal(t, p.Name, "my_project")
 	})
@@ -41,7 +42,7 @@ func TestProjectName(t *testing.T) {
 	t.Run("by name start with number", func(t *testing.T) {
 		opts, err := NewProjectOptions([]string{"testdata/simple/compose.yaml"}, WithName("42my_project_num"))
 		assert.NilError(t, err)
-		p, err := ProjectFromOptions(opts)
+		p, err := ProjectFromOptions(context.TODO(), opts)
 		assert.NilError(t, err)
 		assert.Equal(t, p.Name, "42my_project_num")
 
@@ -49,7 +50,7 @@ func TestProjectName(t *testing.T) {
 			fmt.Sprintf("%s=%s", consts.ComposeProjectName, "42my_project_env"),
 		}))
 		assert.NilError(t, err)
-		p, err = ProjectFromOptions(opts)
+		p, err = ProjectFromOptions(context.TODO(), opts)
 		assert.NilError(t, err)
 		assert.Equal(t, p.Name, "42my_project_env")
 	})
@@ -60,7 +61,7 @@ func TestProjectName(t *testing.T) {
 			WithName(""),
 		)
 		assert.NilError(t, err)
-		p, err := ProjectFromOptions(opts)
+		p, err := ProjectFromOptions(context.TODO(), opts)
 		assert.NilError(t, err)
 		assert.Equal(t, p.Name, "simple")
 	})
@@ -72,7 +73,7 @@ func TestProjectName(t *testing.T) {
 			WithWorkingDirectory("/path/to/proj"),
 		)
 		assert.NilError(t, err)
-		p, err := ProjectFromOptions(opts)
+		p, err := ProjectFromOptions(context.TODO(), opts)
 		assert.NilError(t, err)
 		assert.Equal(t, p.Name, "proj")
 	})
@@ -81,7 +82,7 @@ func TestProjectName(t *testing.T) {
 		opts, err := NewProjectOptions([]string{"testdata/simple/compose.yaml"},
 			WithWorkingDirectory("/"))
 		assert.NilError(t, err)
-		p, err := ProjectFromOptions(opts)
+		p, err := ProjectFromOptions(context.TODO(), opts)
 
 		// root directory will resolve to an empty project name since there
 		// IS no directory name!
@@ -97,7 +98,7 @@ func TestProjectName(t *testing.T) {
 			fmt.Sprintf("%s=%s", consts.ComposeProjectName, "-my_project"),
 		}))
 		assert.NilError(t, err)
-		p, err := ProjectFromOptions(opts)
+		p, err := ProjectFromOptions(context.TODO(), opts)
 		assert.ErrorContains(t, err, `invalid project name "-my_project"`)
 		assert.Assert(t, p == nil)
 	})
@@ -110,7 +111,7 @@ func TestProjectName(t *testing.T) {
 			fmt.Sprintf("%s=%s", consts.ComposeProjectName, "_my_project"),
 		}))
 		assert.NilError(t, err)
-		p, err := ProjectFromOptions(opts)
+		p, err := ProjectFromOptions(context.TODO(), opts)
 		assert.ErrorContains(t, err, `invalid project name "_my_project"`)
 		assert.Assert(t, p == nil)
 	})
@@ -123,7 +124,7 @@ func TestProjectName(t *testing.T) {
 			fmt.Sprintf("%s=%s", consts.ComposeProjectName, "www.my.project"),
 		}))
 		assert.NilError(t, err)
-		p, err := ProjectFromOptions(opts)
+		p, err := ProjectFromOptions(context.TODO(), opts)
 		assert.ErrorContains(t, err, `invalid project name "www.my.project"`)
 		assert.Assert(t, p == nil)
 	})
@@ -136,7 +137,7 @@ func TestProjectName(t *testing.T) {
 			fmt.Sprintf("%s=%s", consts.ComposeProjectName, "MY_PROJECT"),
 		}))
 		assert.NilError(t, err)
-		p, err := ProjectFromOptions(opts)
+		p, err := ProjectFromOptions(context.TODO(), opts)
 		assert.ErrorContains(t, err, `invalid project name "MY_PROJECT"`)
 		assert.Assert(t, p == nil)
 	})
@@ -144,7 +145,7 @@ func TestProjectName(t *testing.T) {
 	t.Run("by working dir", func(t *testing.T) {
 		opts, err := NewProjectOptions([]string{"testdata/simple/compose.yaml"}, WithWorkingDirectory("."))
 		assert.NilError(t, err)
-		p, err := ProjectFromOptions(opts)
+		p, err := ProjectFromOptions(context.TODO(), opts)
 		assert.NilError(t, err)
 		assert.Equal(t, p.Name, "cli")
 	})
@@ -152,7 +153,7 @@ func TestProjectName(t *testing.T) {
 	t.Run("by compose file parent dir", func(t *testing.T) {
 		opts, err := NewProjectOptions([]string{"testdata/simple/compose.yaml"})
 		assert.NilError(t, err)
-		p, err := ProjectFromOptions(opts)
+		p, err := ProjectFromOptions(context.TODO(), opts)
 		assert.NilError(t, err)
 		assert.Equal(t, p.Name, "simple")
 	})
@@ -160,7 +161,7 @@ func TestProjectName(t *testing.T) {
 	t.Run("by compose file parent dir special", func(t *testing.T) {
 		opts, err := NewProjectOptions([]string{"testdata/UNNORMALIZED PATH/compose.yaml"})
 		assert.NilError(t, err)
-		p, err := ProjectFromOptions(opts)
+		p, err := ProjectFromOptions(context.TODO(), opts)
 		assert.NilError(t, err)
 		assert.Equal(t, p.Name, "unnormalizedpath")
 	})
@@ -170,7 +171,7 @@ func TestProjectName(t *testing.T) {
 		defer os.Unsetenv("COMPOSE_PROJECT_NAME")                //nolint:errcheck
 		opts, err := NewProjectOptions([]string{"testdata/simple/compose.yaml"}, WithOsEnv)
 		assert.NilError(t, err)
-		p, err := ProjectFromOptions(opts)
+		p, err := ProjectFromOptions(context.TODO(), opts)
 		assert.NilError(t, err)
 		assert.Equal(t, p.Name, "my_project_from_env")
 	})
@@ -184,7 +185,7 @@ func TestProjectName(t *testing.T) {
 
 		opts, err := NewProjectOptions(nil, WithEnvFiles(), WithDotEnv, WithConfigFileEnv)
 		assert.NilError(t, err)
-		p, err := ProjectFromOptions(opts)
+		p, err := ProjectFromOptions(context.TODO(), opts)
 		assert.NilError(t, err)
 		assert.Equal(t, p.Name, "my_project_from_dot_env")
 	})
@@ -194,7 +195,7 @@ func TestProjectName(t *testing.T) {
 			"TEST=expected",
 		}))
 		assert.NilError(t, err)
-		p, err := ProjectFromOptions(opts)
+		p, err := ProjectFromOptions(context.TODO(), opts)
 		assert.NilError(t, err)
 		assert.Equal(t, p.Name, "test-expected-test")
 	})
@@ -206,7 +207,7 @@ func TestProjectFromSetOfFiles(t *testing.T) {
 		"testdata/simple/compose-with-overrides.yaml",
 	}, WithName("my_project"))
 	assert.NilError(t, err)
-	p, err := ProjectFromOptions(opts)
+	p, err := ProjectFromOptions(context.TODO(), opts)
 	assert.NilError(t, err)
 	service, err := p.GetService("simple")
 	assert.NilError(t, err)
@@ -220,7 +221,7 @@ func TestProjectComposefilesFromSetOfFiles(t *testing.T) {
 		WithDefaultConfigPath,
 	)
 	assert.NilError(t, err)
-	p, err := ProjectFromOptions(opts)
+	p, err := ProjectFromOptions(context.TODO(), opts)
 	assert.NilError(t, err)
 	absPath, _ := filepath.Abs(filepath.Join("testdata", "simple", "compose.yaml"))
 	assert.DeepEqual(t, p.ComposeFiles, []string{absPath})
@@ -232,7 +233,7 @@ func TestProjectComposefilesFromWorkingDir(t *testing.T) {
 		"testdata/simple/compose-with-overrides.yaml",
 	}, WithName("my_project"))
 	assert.NilError(t, err)
-	p, err := ProjectFromOptions(opts)
+	p, err := ProjectFromOptions(context.TODO(), opts)
 	assert.NilError(t, err)
 	currentDir, _ := os.Getwd()
 	assert.DeepEqual(t, p.ComposeFiles, []string{
@@ -252,7 +253,7 @@ func TestProjectWithDotEnv(t *testing.T) {
 		"compose-with-variables.yaml",
 	}, WithName("my_project"), WithEnvFiles(), WithDotEnv)
 	assert.NilError(t, err)
-	p, err := ProjectFromOptions(opts)
+	p, err := ProjectFromOptions(context.TODO(), opts)
 	assert.NilError(t, err)
 	service, err := p.GetService("simple")
 	assert.NilError(t, err)
@@ -265,7 +266,7 @@ func TestProjectWithDiscardEnvFile(t *testing.T) {
 	}, WithDiscardEnvFile)
 
 	assert.NilError(t, err)
-	p, err := ProjectFromOptions(opts)
+	p, err := ProjectFromOptions(context.TODO(), opts)
 	assert.NilError(t, err)
 	service, err := p.GetService("simple")
 	assert.NilError(t, err)
@@ -282,7 +283,7 @@ func TestProjectWithMultipleEnvFile(t *testing.T) {
 		WithDotEnv)
 
 	assert.NilError(t, err)
-	p, err := ProjectFromOptions(opts)
+	p, err := ProjectFromOptions(context.TODO(), opts)
 	assert.NilError(t, err)
 	service, err := p.GetService("simple")
 	assert.NilError(t, err)
@@ -296,7 +297,7 @@ func TestProjectNameFromWorkingDir(t *testing.T) {
 		"testdata/env-file/compose-with-env-file.yaml",
 	})
 	assert.NilError(t, err)
-	p, err := ProjectFromOptions(opts)
+	p, err := ProjectFromOptions(context.TODO(), opts)
 	assert.NilError(t, err)
 	assert.Equal(t, p.Name, "env-file")
 }
diff --git a/cli/options_windows_test.go b/cli/options_windows_test.go
index 21748f1..15856c4 100644
--- a/cli/options_windows_test.go
+++ b/cli/options_windows_test.go
@@ -17,6 +17,7 @@
 package cli
 
 import (
+	"context"
 	"os"
 	"testing"
 
@@ -31,7 +32,7 @@ func TestConvertWithEnvVar(t *testing.T) {
 		WithWorkingDirectory("C:\\project-dir\\"),
 		WithResolvedPaths(true))
 
-	p, err := ProjectFromOptions(opts)
+	p, err := ProjectFromOptions(context.TODO(), opts)
 
 	assert.NilError(t, err)
 	volumes := p.Services["test"].Volumes
diff --git a/loader/loader_test.go b/loader/loader_test.go
index 0e44bbc..3e926b2 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -70,7 +70,7 @@ func loadYAML(yaml string) (*types.Project, error) {
 }
 
 func loadYAMLWithEnv(yaml string, env map[string]string) (*types.Project, error) {
-	return Load(buildConfigDetails(yaml, env), func(options *Options) {
+	return LoadWithContext(context.TODO(), buildConfigDetails(yaml, env), func(options *Options) {
 		options.SkipConsistencyCheck = true
 		options.SkipNormalization = true
 		options.ResolvePaths = true