Repository: compose-spec/compose-go
Pull request #372 (2023-03-23 20:48:37)

cli: allow setting empty name in options (to infer)
The `ProjectOptions` includes config to pass to the loader, but most of the fields, including `Name`, are optional. If unset/empty, the loader will determine a project name (if possible).

Allow (re-)setting it to empty here. This is mostly for convenience based on the way these helpers are used in practice, so that it's possible to pass a value from e.g. a CLI flag in, which will be empty if unset.

Also added a bunch of documentation to the type.


Patch:
diff --git a/cli/options.go b/cli/options.go
index ea22b00..d9b5bce 100644
--- a/cli/options.go
+++ b/cli/options.go
@@ -23,23 +23,59 @@ import (
 	"path/filepath"
 	"strings"
 
+	"github.com/pkg/errors"
+	"github.com/sirupsen/logrus"
+
 	"github.com/compose-spec/compose-go/consts"
 	"github.com/compose-spec/compose-go/dotenv"
 	"github.com/compose-spec/compose-go/errdefs"
 	"github.com/compose-spec/compose-go/loader"
 	"github.com/compose-spec/compose-go/types"
 	"github.com/compose-spec/compose-go/utils"
-	"github.com/pkg/errors"
-	"github.com/sirupsen/logrus"
 )
 
-// ProjectOptions groups the command line options recommended for a Compose implementation
+// ProjectOptions provides common configuration for loading a project.
 type ProjectOptions struct {
-	Name        string
-	WorkingDir  string
+	// Name is a valid Compose project name to be used or empty.
+	//
+	// If empty, the project loader will automatically infer a reasonable
+	// project name if possible.
+	Name string
+
+	// WorkingDir is a file path to use as the project directory or empty.
+	//
+	// If empty, the project loader will automatically infer a reasonable
+	// working directory if possible.
+	WorkingDir string
+
+	// ConfigPaths are file paths to one or more Compose files.
+	//
+	// These are applied in order by the loader following the merge logic
+	// as described in the spec.
+	//
+	// The first entry is required and is the primary Compose file.
+	// For convenience, WithConfigFileEnv and WithDefaultConfigPath
+	// are provided to populate this in a predictable manner.
 	ConfigPaths []string
+
+	// Environment are additional environment variables to make available
+	// for interpolation.
+	//
+	// NOTE: For security, the loader does not automatically expose any
+	// process environment variables. For convenience, WithOsEnv can be
+	// used if appropriate.
 	Environment map[string]string
-	EnvFiles    []string
+
+	// EnvFiles are file paths to ".env" files with additional environment
+	// variable data.
+	//
+	// These are loaded in-order, so it is possible to override variables or
+	// in subsequent files.
+	//
+	// This field is optional, but any file paths that are included here must
+	// exist or an error will be returned during load.
+	EnvFiles []string
+
 	loadOptions []func(*loader.Options)
 }
 
@@ -63,9 +99,15 @@ func NewProjectOptions(configs []string, opts ...ProjectOptionsFn) (*ProjectOpti
 // WithName defines ProjectOptions' name
 func WithName(name string) ProjectOptionsFn {
 	return func(o *ProjectOptions) error {
-		normalized := loader.NormalizeProjectName(name)
-		if err := loader.CheckOriginalProjectNameIsNormalized(name, normalized); err != nil {
-			return err
+		// a project (once loaded) cannot have an empty name
+		// however, on the options object, the name is optional: if unset,
+		// a name will be inferred by the loader, so it's legal to set the
+		// name to an empty string here
+		if name != "" {
+			normalized := loader.NormalizeProjectName(name)
+			if err := loader.CheckOriginalProjectNameIsNormalized(name, normalized); err != nil {
+				return err
+			}
 		}
 		o.Name = name
 		return nil


Test patch:
diff --git a/cli/options_test.go b/cli/options_test.go
index 7260f65..a594581 100644
--- a/cli/options_test.go
+++ b/cli/options_test.go
@@ -22,9 +22,10 @@ import (
 	"path/filepath"
 	"testing"
 
+	"gotest.tools/v3/assert"
+
 	"github.com/compose-spec/compose-go/consts"
 	"github.com/compose-spec/compose-go/utils"
-	"gotest.tools/v3/assert"
 )
 
 func TestProjectName(t *testing.T) {
@@ -52,9 +53,27 @@ func TestProjectName(t *testing.T) {
 		assert.Equal(t, p.Name, "42my_project_env")
 	})
 
-	t.Run("by name must not be empty", func(t *testing.T) {
-		_, err := NewProjectOptions([]string{"testdata/simple/compose.yaml"}, WithName(""))
-		assert.ErrorContains(t, err, `project name must not be empty`)
+	t.Run("by name empty", func(t *testing.T) {
+		opts, err := NewProjectOptions(
+			[]string{"testdata/simple/compose.yaml"},
+			WithName(""),
+		)
+		assert.NilError(t, err)
+		p, err := ProjectFromOptions(opts)
+		assert.NilError(t, err)
+		assert.Equal(t, p.Name, "simple")
+	})
+
+	t.Run("by name empty working dir", func(t *testing.T) {
+		opts, err := NewProjectOptions(
+			[]string{"testdata/simple/compose.yaml"},
+			WithName(""),
+			WithWorkingDirectory("/path/to/proj"),
+		)
+		assert.NilError(t, err)
+		p, err := ProjectFromOptions(opts)
+		assert.NilError(t, err)
+		assert.Equal(t, p.Name, "proj")
 	})
 
 	t.Run("by name must not come from root directory", func(t *testing.T) {