From 0eb18c82eb342cf15f4d034413cdefc390a1ecf6 Mon Sep 17 00:00:00 2001 From: Daniel Mikusa Date: Sun, 23 Jun 2024 16:09:05 -0400 Subject: [PATCH] Make buildpack API generic Instead of using `interface{}` for things like metadata, we can use generic types. The buildpack API doesn't care what's in those values, so it can just be an `any` type. This will allow buildpack authors to set the types they want to use and even use custom types for their metadata. Then when accessing the metadata in their buildpacks, no casting is required. This reduces the amount of code and makes the code less error prone. Signed-off-by: Daniel Mikusa --- build.go | 55 ++++++++++------------ build_plan.go | 14 +++--- build_test.go | 96 +++++++++++++++++++-------------------- buildpack.go | 4 +- buildpack_plan.go | 8 ++-- buildpack_test.go | 2 +- detect.go | 18 ++++---- detect_test.go | 68 +++++++++++++-------------- examples/build_test.go | 16 +++---- examples/detect_test.go | 16 +++---- examples/generate_test.go | 4 +- extension.go | 4 +- extension_test.go | 2 +- generate.go | 12 ++--- generate_test.go | 26 +++++------ layer.go | 24 +++++----- layer_test.go | 24 +++++----- main.go | 8 ++-- main_test.go | 32 ++++++------- store.go | 4 +- 20 files changed, 215 insertions(+), 222 deletions(-) diff --git a/build.go b/build.go index 1d581a3..13cd291 100644 --- a/build.go +++ b/build.go @@ -22,6 +22,7 @@ import ( "os" "path/filepath" "reflect" + "slices" "strings" "github.com/BurntSushi/toml" @@ -32,25 +33,25 @@ import ( ) // BuildContext contains the inputs to build. -type BuildContext struct { +type BuildContext[PL any, PM any, LM any, BM any] struct { // ApplicationPath is the location of the application source code as provided by // the lifecycle. ApplicationPath string // Buildpack is metadata about the buildpack, from buildpack.toml. - Buildpack Buildpack + Buildpack Buildpack[BM] // Layers is the layers available to the buildpack. - Layers Layers + Layers Layers[LM] // Logger is the way to write messages to the end user Logger log.Logger // PersistentMetadata is metadata that is persisted even across cache cleaning. - PersistentMetadata map[string]interface{} + PersistentMetadata map[string]PM // Plan is the buildpack plan provided to the buildpack. - Plan BuildpackPlan + Plan BuildpackPlan[PL] // Platform is the contents of the platform. Platform Platform @@ -66,15 +67,15 @@ type BuildContext struct { } // BuildResult contains the results of detection. -type BuildResult struct { +type BuildResult[PM any, LM any] struct { // Labels are the image labels contributed by the buildpack. Labels []Label // Layers is the collection of LayerCreators contributed by the buildpack. - Layers []Layer + Layers []Layer[LM] // PersistentMetadata is metadata that is persisted even across cache cleaning. - PersistentMetadata map[string]interface{} + PersistentMetadata map[string]PM // Processes are the process types contributed by the buildpack. Processes []Process @@ -97,13 +98,13 @@ const ( ) // NewBuildResult creates a new BuildResult instance, initializing empty fields. -func NewBuildResult() BuildResult { - return BuildResult{ - PersistentMetadata: make(map[string]interface{}), +func NewBuildResult[PM any, LM any]() BuildResult[PM, LM] { + return BuildResult[PM, LM]{ + PersistentMetadata: make(map[string]PM), } } -func (b BuildResult) String() string { +func (b BuildResult[PM, LM]) String() string { var l []string for _, c := range b.Layers { l = append(l, reflect.TypeOf(c).Name()) @@ -116,16 +117,20 @@ func (b BuildResult) String() string { } // BuildFunc takes a context and returns a result, performing buildpack build behaviors. -type BuildFunc func(context BuildContext) (BuildResult, error) +type BuildFunc[PL any, PM any, LM any, BM any] func(context BuildContext[PL, PM, LM, BM]) (BuildResult[PM, LM], error) + +func EmptyBuildFunc[BPL any, PM any, LM any, BM any](context BuildContext[BPL, PM, LM, BM]) (BuildResult[PM, LM], error) { + return BuildResult[PM, LM]{}, nil +} // Build is called by the main function of a buildpack, for build. -func Build(build BuildFunc, config Config) { +func Build[PL any, PM any, LM any, BM any](build BuildFunc[PL, PM, LM, BM], config Config) { var ( err error file string ok bool ) - ctx := BuildContext{Logger: config.logger} + ctx := BuildContext[PL, PM, LM, BM]{Logger: config.logger} ctx.ApplicationPath, err = os.Getwd() if err != nil { @@ -181,7 +186,7 @@ func Build(build BuildFunc, config Config) { config.exitHandler.Error(fmt.Errorf("expected CNB_LAYERS_DIR to be set")) return } - ctx.Layers = Layers{layersDir} + ctx.Layers = Layers[LM]{layersDir} ctx.Platform.Path, ok = os.LookupEnv(EnvPlatformDirectory) if !ok { @@ -216,7 +221,7 @@ func Build(build BuildFunc, config Config) { } config.logger.Debugf("Platform Environment: %s", ctx.Platform.Environment) - var store Store + var store Store[PM] file = filepath.Join(ctx.Layers.Path, "store.toml") if _, err = toml.DecodeFile(file, &store); err != nil && !os.IsNotExist(err) { config.exitHandler.Error(fmt.Errorf("unable to decode persistent metadata %s\n%w", file, err)) @@ -297,7 +302,7 @@ func Build(build BuildFunc, config Config) { } for _, e := range existing { - if strings.HasSuffix(e, "store.toml") || contains(contributed, e) { + if strings.HasSuffix(e, "store.toml") || slices.Contains(contributed, e) { continue } @@ -345,7 +350,7 @@ func Build(build BuildFunc, config Config) { } if len(result.PersistentMetadata) > 0 { - store = Store{ + store = Store[PM]{ Metadata: result.PersistentMetadata, } file = filepath.Join(ctx.Layers.Path, "store.toml") @@ -357,16 +362,6 @@ func Build(build BuildFunc, config Config) { } } -func contains(candidates []string, s string) bool { - for _, c := range candidates { - if s == c { - return true - } - } - - return false -} - func validateSBOMFormats(layersPath string, acceptedSBOMFormats []string) error { sbomFiles, err := filepath.Glob(filepath.Join(layersPath, "*.sbom.*")) if err != nil { @@ -383,7 +378,7 @@ func validateSBOMFormats(layersPath string, acceptedSBOMFormats []string) error return fmt.Errorf("unable to parse SBOM %s\n%w", sbomFormat, err) } - if !contains(acceptedSBOMFormats, sbomFormat.MediaType()) { + if !slices.Contains(acceptedSBOMFormats, sbomFormat.MediaType()) { return fmt.Errorf("unable to find actual SBOM Type %s in list of supported SBOM types %s", sbomFormat.MediaType(), acceptedSBOMFormats) } } diff --git a/build_plan.go b/build_plan.go index 047be8c..4d91cfa 100644 --- a/build_plan.go +++ b/build_plan.go @@ -23,28 +23,28 @@ type BuildPlanProvide struct { } // BuildPlanRequire represents a dependency required by a buildpack. -type BuildPlanRequire struct { +type BuildPlanRequire[PL any] struct { // Name is the name of the dependency. Name string `toml:"name"` // Metadata is the metadata for the dependency. Optional. - Metadata map[string]interface{} `toml:"metadata,omitempty"` + Metadata map[string]PL `toml:"metadata,omitempty"` } // BuildPlan represents the provisions and requirements of a buildpack during detection. -type BuildPlan struct { +type BuildPlan[PL any] struct { // Provides is the dependencies provided by the buildpack. Provides []BuildPlanProvide `toml:"provides,omitempty"` // Requires is the dependencies required by the buildpack. - Requires []BuildPlanRequire `toml:"requires,omitempty"` + Requires []BuildPlanRequire[PL] `toml:"requires,omitempty"` } // BuildPlans represents a collection of build plans produced by a buildpack during detection. -type BuildPlans struct { +type BuildPlans[PL any] struct { // BuildPlan is the first build plan. - BuildPlan + BuildPlan[PL] // Or is the collection of other build plans. - Or []BuildPlan `toml:"or,omitempty"` + Or []BuildPlan[PL] `toml:"or,omitempty"` } diff --git a/build_test.go b/build_test.go index bc527ee..b824479 100644 --- a/build_test.go +++ b/build_test.go @@ -38,7 +38,7 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { var ( Expect = NewWithT(t).Expect - buildFunc libcnb.BuildFunc + buildFunc libcnb.BuildFunc[string, string, string, string] applicationPath string buildpackPath string buildpackPlanPath string @@ -55,8 +55,8 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { ) it.Before(func() { - buildFunc = func(libcnb.BuildContext) (libcnb.BuildResult, error) { - return libcnb.NewBuildResult(), nil + buildFunc = func(libcnb.BuildContext[string, string, string, string]) (libcnb.BuildResult[string, string], error) { + return libcnb.NewBuildResult[string, string](), nil } var err error @@ -265,7 +265,7 @@ version = "1.1.1" }) context("has a build environment", func() { - var ctx libcnb.BuildContext + var ctx libcnb.BuildContext[string, string, string, string] it.Before(func() { Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), @@ -280,9 +280,9 @@ version = "1.1.1" 0600), ).To(Succeed()) - buildFunc = func(context libcnb.BuildContext) (libcnb.BuildResult, error) { + buildFunc = func(context libcnb.BuildContext[string, string, string, string]) (libcnb.BuildResult[string, string], error) { ctx = context - return libcnb.NewBuildResult(), nil + return libcnb.NewBuildResult[string, string](), nil } }) @@ -293,7 +293,7 @@ version = "1.1.1" ) Expect(ctx.ApplicationPath).To(Equal(applicationPath)) - Expect(ctx.Buildpack).To(Equal(libcnb.Buildpack{ + Expect(ctx.Buildpack).To(Equal(libcnb.Buildpack[string]{ API: "0.8", Info: libcnb.BuildpackInfo{ ID: "test-id", @@ -302,13 +302,13 @@ version = "1.1.1" }, Path: buildpackPath, })) - Expect(ctx.Layers).To(Equal(libcnb.Layers{Path: layersPath})) - Expect(ctx.PersistentMetadata).To(Equal(map[string]interface{}{"test-key": "test-value"})) - Expect(ctx.Plan).To(Equal(libcnb.BuildpackPlan{ - Entries: []libcnb.BuildpackPlanEntry{ + Expect(ctx.Layers).To(Equal(libcnb.Layers[string]{Path: layersPath})) + Expect(ctx.PersistentMetadata).To(Equal(map[string]string{"test-key": "test-value"})) + Expect(ctx.Plan).To(Equal(libcnb.BuildpackPlan[string]{ + Entries: []libcnb.BuildpackPlanEntry[string]{ { Name: "test-name", - Metadata: map[string]interface{}{ + Metadata: map[string]string{ "test-key": "test-value", }, }, @@ -332,7 +332,7 @@ version = "1.1.1" }) context("has a build environment specifying target metadata", func() { - var ctx libcnb.BuildContext + var ctx libcnb.BuildContext[string, string, string, string] it.Before(func() { Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), @@ -362,9 +362,9 @@ version = "1.1.1" `), 0600), ).To(Succeed()) - buildFunc = func(context libcnb.BuildContext) (libcnb.BuildResult, error) { + buildFunc = func(context libcnb.BuildContext[string, string, string, string]) (libcnb.BuildResult[string, string], error) { ctx = context - return libcnb.NewBuildResult(), nil + return libcnb.NewBuildResult[string, string](), nil } }) @@ -408,8 +408,8 @@ version = "1.1.1" }) it("handles error from BuildFunc", func() { - buildFunc = func(libcnb.BuildContext) (libcnb.BuildResult, error) { - return libcnb.NewBuildResult(), errors.New("test-error") + buildFunc = func(libcnb.BuildContext[string, string, string, string]) (libcnb.BuildResult[string, string], error) { + return libcnb.NewBuildResult[string, string](), errors.New("test-error") } libcnb.Build(buildFunc, @@ -423,10 +423,10 @@ version = "1.1.1" }) it("writes env.build", func() { - buildFunc = func(libcnb.BuildContext) (libcnb.BuildResult, error) { - layer := libcnb.Layer{Path: filepath.Join(layersPath, "test-name"), BuildEnvironment: libcnb.Environment{}} + buildFunc = func(libcnb.BuildContext[string, string, string, string]) (libcnb.BuildResult[string, string], error) { + layer := libcnb.Layer[string]{Path: filepath.Join(layersPath, "test-name"), BuildEnvironment: libcnb.Environment{}} layer.BuildEnvironment.Defaultf("test-build", "test-%s", "value") - return libcnb.BuildResult{Layers: []libcnb.Layer{layer}}, nil + return libcnb.BuildResult[string, string]{Layers: []libcnb.Layer[string]{layer}}, nil } libcnb.Build(buildFunc, @@ -441,10 +441,10 @@ version = "1.1.1" }) it("writes env.launch", func() { - buildFunc = func(libcnb.BuildContext) (libcnb.BuildResult, error) { - layer := libcnb.Layer{Path: filepath.Join(layersPath, "test-name"), LaunchEnvironment: libcnb.Environment{}} + buildFunc = func(libcnb.BuildContext[string, string, string, string]) (libcnb.BuildResult[string, string], error) { + layer := libcnb.Layer[string]{Path: filepath.Join(layersPath, "test-name"), LaunchEnvironment: libcnb.Environment{}} layer.LaunchEnvironment.Defaultf("test-launch", "test-%s", "value") - return libcnb.BuildResult{Layers: []libcnb.Layer{layer}}, nil + return libcnb.BuildResult[string, string]{Layers: []libcnb.Layer[string]{layer}}, nil } libcnb.Build(buildFunc, @@ -459,10 +459,10 @@ version = "1.1.1" }) it("writes env", func() { - buildFunc = func(libcnb.BuildContext) (libcnb.BuildResult, error) { - layer := libcnb.Layer{Path: filepath.Join(layersPath, "test-name"), SharedEnvironment: libcnb.Environment{}} + buildFunc = func(libcnb.BuildContext[string, string, string, string]) (libcnb.BuildResult[string, string], error) { + layer := libcnb.Layer[string]{Path: filepath.Join(layersPath, "test-name"), SharedEnvironment: libcnb.Environment{}} layer.SharedEnvironment.Defaultf("test-shared", "test-%s", "value") - return libcnb.BuildResult{Layers: []libcnb.Layer{layer}}, nil + return libcnb.BuildResult[string, string]{Layers: []libcnb.Layer[string]{layer}}, nil } libcnb.Build(buildFunc, @@ -477,8 +477,8 @@ version = "1.1.1" }) it("writes layer metadata", func() { - buildFunc = func(libcnb.BuildContext) (libcnb.BuildResult, error) { - layer := libcnb.Layer{ + buildFunc = func(libcnb.BuildContext[string, string, string, string]) (libcnb.BuildResult[string, string], error) { + layer := libcnb.Layer[string]{ Name: "test-name", Path: filepath.Join(layersPath, "test-name"), LayerTypes: libcnb.LayerTypes{ @@ -486,9 +486,9 @@ version = "1.1.1" Cache: true, Launch: true, }, - Metadata: map[string]interface{}{"test-key": "test-value"}, + Metadata: map[string]string{"test-key": "test-value"}, } - return libcnb.BuildResult{Layers: []libcnb.Layer{layer}}, nil + return libcnb.BuildResult[string, string]{Layers: []libcnb.Layer[string]{layer}}, nil } libcnb.Build(buildFunc, @@ -500,12 +500,12 @@ version = "1.1.1" Expect(tomlWriter.Calls[0].Arguments[0]).To(Equal(filepath.Join(layersPath, "test-name.toml"))) - layer, ok := tomlWriter.Calls[0].Arguments[1].(libcnb.Layer) + layer, ok := tomlWriter.Calls[0].Arguments[1].(libcnb.Layer[string]) Expect(ok).To(BeTrue()) Expect(layer.LayerTypes.Build).To(BeTrue()) Expect(layer.LayerTypes.Cache).To(BeTrue()) Expect(layer.LayerTypes.Launch).To(BeTrue()) - Expect(layer.Metadata).To(Equal(map[string]interface{}{"test-key": "test-value"})) + Expect(layer.Metadata).To(Equal(map[string]string{"test-key": "test-value"})) }) it("writes launch.toml with working-directory setting", func() { @@ -515,9 +515,9 @@ version = "1.1.1" Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), b.Bytes(), 0600)).To(Succeed()) - buildFunc = func(libcnb.BuildContext) (libcnb.BuildResult, error) { - return libcnb.BuildResult{ - Layers: []libcnb.Layer{}, + buildFunc = func(libcnb.BuildContext[string, string, string, string]) (libcnb.BuildResult[string, string], error) { + return libcnb.BuildResult[string, string]{ + Layers: []libcnb.Layer[string]{}, Processes: []libcnb.Process{ { Type: "test-type", @@ -549,8 +549,8 @@ version = "1.1.1" }) it("writes launch.toml", func() { - buildFunc = func(libcnb.BuildContext) (libcnb.BuildResult, error) { - return libcnb.BuildResult{ + buildFunc = func(libcnb.BuildContext[string, string, string, string]) (libcnb.BuildResult[string, string], error) { + return libcnb.BuildResult[string, string]{ Labels: []libcnb.Label{ { Key: "test-key", @@ -603,10 +603,10 @@ version = "1.1.1" }) it("writes persistent metadata", func() { - m := map[string]interface{}{"test-key": "test-value"} + m := map[string]string{"test-key": "test-value"} - buildFunc = func(libcnb.BuildContext) (libcnb.BuildResult, error) { - return libcnb.BuildResult{PersistentMetadata: m}, nil + buildFunc = func(libcnb.BuildContext[string, string, string, string]) (libcnb.BuildResult[string, string], error) { + return libcnb.BuildResult[string, string]{PersistentMetadata: m}, nil } libcnb.Build(buildFunc, @@ -617,7 +617,7 @@ version = "1.1.1" ) Expect(tomlWriter.Calls[0].Arguments[0]).To(Equal(filepath.Join(layersPath, "store.toml"))) - Expect(tomlWriter.Calls[0].Arguments[1]).To(Equal(libcnb.Store{Metadata: m})) + Expect(tomlWriter.Calls[0].Arguments[1]).To(Equal(libcnb.Store[string]{Metadata: m})) }) it("does not write empty files", func() { @@ -636,10 +636,10 @@ version = "1.1.1" Expect(os.WriteFile(filepath.Join(layersPath, "bravo.toml"), []byte(""), 0600)).To(Succeed()) Expect(os.WriteFile(filepath.Join(layersPath, "store.toml"), []byte(""), 0600)).To(Succeed()) - layer := libcnb.Layer{Name: "alpha"} + layer := libcnb.Layer[string]{Name: "alpha"} - buildFunc = func(libcnb.BuildContext) (libcnb.BuildResult, error) { - return libcnb.BuildResult{Layers: []libcnb.Layer{layer}}, nil + buildFunc = func(libcnb.BuildContext[string, string, string, string]) (libcnb.BuildResult[string, string], error) { + return libcnb.BuildResult[string, string]{Layers: []libcnb.Layer[string]{layer}}, nil } libcnb.Build(buildFunc, @@ -656,8 +656,8 @@ version = "1.1.1" }) it("writes build.toml", func() { - buildFunc = func(libcnb.BuildContext) (libcnb.BuildResult, error) { - return libcnb.BuildResult{ + buildFunc = func(libcnb.BuildContext[string, string, string, string]) (libcnb.BuildResult[string, string], error) { + return libcnb.BuildResult[string, string]{ Unmet: []libcnb.UnmetPlanEntry{ { Name: "test-entry", @@ -698,8 +698,8 @@ sbom-formats = ["application/vnd.cyclonedx+json"] 0600), ).To(Succeed()) - buildFunc = func(libcnb.BuildContext) (libcnb.BuildResult, error) { - return libcnb.BuildResult{}, nil + buildFunc = func(libcnb.BuildContext[string, string, string, string]) (libcnb.BuildResult[string, string], error) { + return libcnb.BuildResult[string, string]{}, nil } }) diff --git a/buildpack.go b/buildpack.go index 9a6578c..64e29e8 100644 --- a/buildpack.go +++ b/buildpack.go @@ -112,7 +112,7 @@ type Target struct { } // Buildpack is the contents of the buildpack.toml file. -type Buildpack struct { +type Buildpack[BM any] struct { // API is the api version expected by the buildpack. API string `toml:"api"` @@ -129,5 +129,5 @@ type Buildpack struct { Targets []Target `toml:"targets"` // Metadata is arbitrary metadata attached to the buildpack. - Metadata map[string]interface{} `toml:"metadata"` + Metadata map[string]BM `toml:"metadata"` } diff --git a/buildpack_plan.go b/buildpack_plan.go index 53d7974..c94e78e 100644 --- a/buildpack_plan.go +++ b/buildpack_plan.go @@ -17,19 +17,19 @@ package libcnb // BuildpackPlan represents a buildpack plan. -type BuildpackPlan struct { +type BuildpackPlan[PL any] struct { // Entries represents all the buildpack plan entries. - Entries []BuildpackPlanEntry `toml:"entries,omitempty"` + Entries []BuildpackPlanEntry[PL] `toml:"entries,omitempty"` } // BuildpackPlanEntry represents an entry in the buildpack plan. -type BuildpackPlanEntry struct { +type BuildpackPlanEntry[PL any] struct { // Name represents the name of the entry. Name string `toml:"name"` // Metadata is the metadata of the entry. Optional. - Metadata map[string]interface{} `toml:"metadata,omitempty"` + Metadata map[string]PL `toml:"metadata,omitempty"` } // UnmetPlanEntry denotes an unmet buildpack plan entry. When a buildpack returns an UnmetPlanEntry diff --git a/buildpack_test.go b/buildpack_test.go index d2f320b..dd294bf 100644 --- a/buildpack_test.go +++ b/buildpack_test.go @@ -34,7 +34,7 @@ func testBuildpackTOML(t *testing.T, _ spec.G, it spec.S) { ) it("does not serialize the Path field", func() { - bp := libcnb.Buildpack{ + bp := libcnb.Buildpack[string]{ API: "0.8", Info: libcnb.BuildpackInfo{ ID: "test-buildpack/sample", diff --git a/detect.go b/detect.go index a5e044e..feef891 100644 --- a/detect.go +++ b/detect.go @@ -30,17 +30,17 @@ import ( ) // DetectContext contains the inputs to detection. -type DetectContext struct { +type DetectContext[EM any, BM any] struct { // ApplicationPath is the location of the application source code as provided by // the lifecycle. ApplicationPath string // Buildpack is metadata about the buildpack from buildpack.toml (empty when processing an extension) - Buildpack Buildpack + Buildpack Buildpack[BM] // Extension is metadata about the extension from extension.toml (empty when processing a buildpack) - Extension Extension + Extension Extension[EM] // Logger is the way to write messages to the end user Logger log.Logger @@ -53,20 +53,20 @@ type DetectContext struct { } // DetectResult contains the results of detection. -type DetectResult struct { +type DetectResult[DPL any] struct { // Pass indicates whether detection has passed. Pass bool // Plans are the build plans contributed by the buildpack. - Plans []BuildPlan + Plans []BuildPlan[DPL] } // DetectFunc takes a context and returns a result, performing buildpack detect behaviors. -type DetectFunc func(context DetectContext) (DetectResult, error) +type DetectFunc[DPL any, EM any, BM any] func(context DetectContext[EM, BM]) (DetectResult[DPL], error) // Detect is called by the main function of a buildpack, for detection. -func Detect(detect DetectFunc, config Config) { +func Detect[DPL any, EM any, BM any](detect DetectFunc[DPL, EM, BM], config Config) { var ( err error file string @@ -75,7 +75,7 @@ func Detect(detect DetectFunc, config Config) { path string destination interface{} ) - ctx := DetectContext{Logger: config.logger} + ctx := DetectContext[EM, BM]{Logger: config.logger} var moduletype = "buildpack" if config.extension { @@ -203,7 +203,7 @@ func Detect(detect DetectFunc, config Config) { } if len(result.Plans) > 0 { - var plans BuildPlans + var plans BuildPlans[DPL] if len(result.Plans) > 0 { plans.BuildPlan = result.Plans[0] } diff --git a/detect_test.go b/detect_test.go index ed9349e..61e89b1 100644 --- a/detect_test.go +++ b/detect_test.go @@ -39,7 +39,7 @@ func testDetect(t *testing.T, context spec.G, it spec.S) { buildpackPath string buildPlanPath string commandPath string - detectFunc libcnb.DetectFunc + detectFunc libcnb.DetectFunc[string, string, string] exitHandler *mocks.ExitHandler platformPath string tomlWriter *mocks.TOMLWriter @@ -95,8 +95,8 @@ test-key = "test-value" commandPath = filepath.Join("bin", "detect") - detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { - return libcnb.DetectResult{}, nil + detectFunc = func(libcnb.DetectContext[string, string]) (libcnb.DetectResult[string], error) { + return libcnb.DetectResult[string]{}, nil } exitHandler = &mocks.ExitHandler{} @@ -209,7 +209,7 @@ version = "1.1.1" }) context("has a detect environment", func() { - var ctx libcnb.DetectContext + var ctx libcnb.DetectContext[string, string] it.Before(func() { Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), @@ -224,9 +224,9 @@ version = "1.1.1" 0600), ).To(Succeed()) - detectFunc = func(context libcnb.DetectContext) (libcnb.DetectResult, error) { + detectFunc = func(context libcnb.DetectContext[string, string]) (libcnb.DetectResult[string], error) { ctx = context - return libcnb.DetectResult{}, nil + return libcnb.DetectResult[string]{}, nil } }) @@ -238,7 +238,7 @@ version = "1.1.1" ) Expect(ctx.ApplicationPath).To(Equal(applicationPath)) - Expect(ctx.Buildpack).To(Equal(libcnb.Buildpack{ + Expect(ctx.Buildpack).To(Equal(libcnb.Buildpack[string]{ API: "0.8", Info: libcnb.BuildpackInfo{ ID: "test-id", @@ -278,8 +278,8 @@ version = "1.1.1" }) it("handles error from DetectFunc", func() { - detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { - return libcnb.DetectResult{}, fmt.Errorf("test-error") + detectFunc = func(libcnb.DetectContext[string, string]) (libcnb.DetectResult[string], error) { + return libcnb.DetectResult[string]{}, fmt.Errorf("test-error") } libcnb.Detect(detectFunc, @@ -293,8 +293,8 @@ version = "1.1.1" }) it("does not write empty files", func() { - detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { - return libcnb.DetectResult{Pass: true}, nil + detectFunc = func(libcnb.DetectContext[string, string]) (libcnb.DetectResult[string], error) { + return libcnb.DetectResult[string]{Pass: true}, nil } libcnb.Detect(detectFunc, @@ -309,18 +309,18 @@ version = "1.1.1" }) it("writes one build plan", func() { - detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { - return libcnb.DetectResult{ + detectFunc = func(libcnb.DetectContext[string, string]) (libcnb.DetectResult[string], error) { + return libcnb.DetectResult[string]{ Pass: true, - Plans: []libcnb.BuildPlan{ + Plans: []libcnb.BuildPlan[string]{ { Provides: []libcnb.BuildPlanProvide{ {Name: "test-name"}, }, - Requires: []libcnb.BuildPlanRequire{ + Requires: []libcnb.BuildPlanRequire[string]{ { Name: "test-name", - Metadata: map[string]interface{}{"test-key": "test-value"}, + Metadata: map[string]string{"test-key": "test-value"}, }, }, }, @@ -337,15 +337,15 @@ version = "1.1.1" ) Expect(tomlWriter.Calls[0].Arguments.Get(0)).To(Equal(buildPlanPath)) - Expect(tomlWriter.Calls[0].Arguments.Get(1)).To(Equal(libcnb.BuildPlans{ - BuildPlan: libcnb.BuildPlan{ + Expect(tomlWriter.Calls[0].Arguments.Get(1)).To(Equal(libcnb.BuildPlans[string]{ + BuildPlan: libcnb.BuildPlan[string]{ Provides: []libcnb.BuildPlanProvide{ {Name: "test-name"}, }, - Requires: []libcnb.BuildPlanRequire{ + Requires: []libcnb.BuildPlanRequire[string]{ { Name: "test-name", - Metadata: map[string]interface{}{"test-key": "test-value"}, + Metadata: map[string]string{"test-key": "test-value"}, }, }, }, @@ -353,18 +353,18 @@ version = "1.1.1" }) it("writes two build plans", func() { - detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { - return libcnb.DetectResult{ + detectFunc = func(libcnb.DetectContext[string, string]) (libcnb.DetectResult[string], error) { + return libcnb.DetectResult[string]{ Pass: true, - Plans: []libcnb.BuildPlan{ + Plans: []libcnb.BuildPlan[string]{ { Provides: []libcnb.BuildPlanProvide{ {Name: "test-name-1"}, }, - Requires: []libcnb.BuildPlanRequire{ + Requires: []libcnb.BuildPlanRequire[string]{ { Name: "test-name-1", - Metadata: map[string]interface{}{"test-key-1": "test-value-1"}, + Metadata: map[string]string{"test-key-1": "test-value-1"}, }, }, }, @@ -372,10 +372,10 @@ version = "1.1.1" Provides: []libcnb.BuildPlanProvide{ {Name: "test-name-2"}, }, - Requires: []libcnb.BuildPlanRequire{ + Requires: []libcnb.BuildPlanRequire[string]{ { Name: "test-name-2", - Metadata: map[string]interface{}{"test-key-2": "test-value-2"}, + Metadata: map[string]string{"test-key-2": "test-value-2"}, }, }, }, @@ -392,27 +392,27 @@ version = "1.1.1" ) Expect(tomlWriter.Calls[0].Arguments.Get(0)).To(Equal(buildPlanPath)) - Expect(tomlWriter.Calls[0].Arguments.Get(1)).To(Equal(libcnb.BuildPlans{ - BuildPlan: libcnb.BuildPlan{ + Expect(tomlWriter.Calls[0].Arguments.Get(1)).To(Equal(libcnb.BuildPlans[string]{ + BuildPlan: libcnb.BuildPlan[string]{ Provides: []libcnb.BuildPlanProvide{ {Name: "test-name-1"}, }, - Requires: []libcnb.BuildPlanRequire{ + Requires: []libcnb.BuildPlanRequire[string]{ { Name: "test-name-1", - Metadata: map[string]interface{}{"test-key-1": "test-value-1"}, + Metadata: map[string]string{"test-key-1": "test-value-1"}, }, }, }, - Or: []libcnb.BuildPlan{ + Or: []libcnb.BuildPlan[string]{ { Provides: []libcnb.BuildPlanProvide{ {Name: "test-name-2"}, }, - Requires: []libcnb.BuildPlanRequire{ + Requires: []libcnb.BuildPlanRequire[string]{ { Name: "test-name-2", - Metadata: map[string]interface{}{"test-key-2": "test-value-2"}, + Metadata: map[string]string{"test-key-2": "test-value-2"}, }, }, }, diff --git a/examples/build_test.go b/examples/build_test.go index 23abd74..1ed5a26 100644 --- a/examples/build_test.go +++ b/examples/build_test.go @@ -21,10 +21,10 @@ type Builder struct { // BuildpackPlan may contain multiple entries for a single buildpack, resolve // into a single entry. -func resolve(plan libcnb.BuildpackPlan, name string) libcnb.BuildpackPlanEntry { - entry := libcnb.BuildpackPlanEntry{ +func resolve(plan libcnb.BuildpackPlan[string], name string) libcnb.BuildpackPlanEntry[string] { + entry := libcnb.BuildpackPlanEntry[string]{ Name: name, - Metadata: map[string]interface{}{}, + Metadata: map[string]string{}, } for _, e := range plan.Entries { for k, v := range e.Metadata { @@ -34,10 +34,10 @@ func resolve(plan libcnb.BuildpackPlan, name string) libcnb.BuildpackPlanEntry { return entry } -func populateLayer(layer libcnb.Layer, version string) (libcnb.Layer, error) { +func populateLayer(layer libcnb.Layer[string], version string) (libcnb.Layer[string], error) { exampleFile := filepath.Join(layer.Path, "example.txt") if err := os.WriteFile(exampleFile, []byte(version), 0600); err != nil { - return libcnb.Layer{}, fmt.Errorf("unable to write example file: %w", err) + return libcnb.Layer[string]{}, fmt.Errorf("unable to write example file: %w", err) } layer.SharedEnvironment.Default("EXAMPLE_FILE", exampleFile) @@ -64,15 +64,15 @@ func populateLayer(layer libcnb.Layer, version string) (libcnb.Layer, error) { return layer, nil } -func (b Builder) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { +func (b Builder) Build(context libcnb.BuildContext[string, string, string, string]) (libcnb.BuildResult[string, string], error) { // Reduce possible multiple buildpack plan entries to a single entry entry := resolve(context.Plan, Provides) - result := libcnb.NewBuildResult() + result := libcnb.NewBuildResult[string, string]() // Read metadata from the buildpack plan, often contributed by libcnb.Requires // of the Detect phase version := DefaultVersion - if v, ok := entry.Metadata["version"].(string); ok { + if v, ok := entry.Metadata["version"]; ok { version = v } diff --git a/examples/detect_test.go b/examples/detect_test.go index e2825d3..eb8fb22 100644 --- a/examples/detect_test.go +++ b/examples/detect_test.go @@ -18,29 +18,29 @@ type Detector struct { Logger log.Logger } -func (Detector) Detect(context libcnb.DetectContext) (libcnb.DetectResult, error) { +func (Detector) Detect(context libcnb.DetectContext[string, string]) (libcnb.DetectResult[string], error) { version := "1.0" // Scan the application source folder to see if the example buildpack is // required. If `version.toml` does not exist we return a failed DetectResult // but no runtime error has occurred, so we return an empty error. versionPath := filepath.Join(context.ApplicationPath, "version.toml") if _, err := os.Open(versionPath); errors.Is(err, os.ErrNotExist) { - return libcnb.DetectResult{}, nil + return libcnb.DetectResult[string]{}, nil } // Read the version number from the buildpack definition if exampleVersion, exists := context.Buildpack.Metadata["version"]; exists { - version = exampleVersion.(string) + version = exampleVersion } // Accept version number from the environment if the user provides it if exampleVersion, exists := context.Platform.Environment[BpExampleVersion]; exists { version = exampleVersion } - metadata := map[string]interface{}{ + metadata := map[string]string{ "version": version, } - return libcnb.DetectResult{ + return libcnb.DetectResult[string]{ Pass: true, - Plans: []libcnb.BuildPlan{ + Plans: []libcnb.BuildPlan[string]{ { // Let the system know that if other buildpacks Require "example" // then this buildpack Provides the implementation logic. @@ -50,7 +50,7 @@ func (Detector) Detect(context libcnb.DetectContext) (libcnb.DetectResult, error // It is common for a buildpack to Require itself if the build phase // needs information from the detect phase. Here we pass the version number // as metadata to the build phase. - Requires: []libcnb.BuildPlanRequire{ + Requires: []libcnb.BuildPlanRequire[string]{ { Name: Provides, Metadata: metadata, @@ -63,5 +63,5 @@ func (Detector) Detect(context libcnb.DetectContext) (libcnb.DetectResult, error func ExampleDetect() { detector := Detector{log.New(os.Stdout)} - libcnb.BuildpackMain(detector.Detect, nil) + libcnb.BuildpackMain(detector.Detect, libcnb.EmptyBuildFunc[string, string, string]) } diff --git a/examples/generate_test.go b/examples/generate_test.go index 255e5d6..ec0f88d 100644 --- a/examples/generate_test.go +++ b/examples/generate_test.go @@ -12,7 +12,7 @@ type Generator struct { Logger log.Logger } -func (Generator) Generate(context libcnb.GenerateContext) (libcnb.GenerateResult, error) { +func (Generator) Generate(context libcnb.GenerateContext[string, string]) (libcnb.GenerateResult, error) { // here you can read the context.ApplicationPath folder // and create run.Dockerfile and build.Dockerfile in the context.OutputPath folder // and read metadata from the context.Extension struct @@ -26,5 +26,5 @@ func (Generator) Generate(context libcnb.GenerateContext) (libcnb.GenerateResult func ExampleGenerate() { generator := Generator{log.New(os.Stdout)} - libcnb.ExtensionMain(nil, generator.Generate) + libcnb.ExtensionMain[string, string, string, string, string, string](nil, generator.Generate) } diff --git a/extension.go b/extension.go index d100799..3e83418 100644 --- a/extension.go +++ b/extension.go @@ -41,7 +41,7 @@ type ExtensionInfo struct { } // Extension is the contents of the extension.toml file. -type Extension struct { +type Extension[EM any] struct { // API is the api version expected by the extension. API string `toml:"api"` @@ -55,5 +55,5 @@ type Extension struct { Targets []Target `toml:"targets"` // Metadata is arbitrary metadata attached to the extension. - Metadata map[string]interface{} `toml:"metadata"` + Metadata map[string]EM `toml:"metadata"` } diff --git a/extension_test.go b/extension_test.go index 079b489..6d51d7c 100644 --- a/extension_test.go +++ b/extension_test.go @@ -34,7 +34,7 @@ func testExtensionTOML(t *testing.T, _ spec.G, it spec.S) { ) it("does not serialize the Path field", func() { - extn := libcnb.Extension{ + extn := libcnb.Extension[string]{ API: "0.8", Info: libcnb.ExtensionInfo{ ID: "test-buildpack/sample", diff --git a/generate.go b/generate.go index 059db3b..c969af0 100644 --- a/generate.go +++ b/generate.go @@ -30,13 +30,13 @@ import ( ) // GenerateContext contains the inputs to generate. -type GenerateContext struct { +type GenerateContext[PL any, EM any] struct { // ApplicationPath is the location of the application source code as provided by // the lifecycle. ApplicationPath string // Extension is metadata about the extension, from extension.toml. - Extension Extension + Extension Extension[EM] // OutputDirectory is the location Dockerfiles should be written to. OutputDirectory string @@ -45,7 +45,7 @@ type GenerateContext struct { Logger log.Logger // Plan is the buildpack plan provided to the buildpack. - Plan BuildpackPlan + Plan BuildpackPlan[PL] // Platform is the contents of the platform. Platform Platform @@ -100,16 +100,16 @@ func (b GenerateResult) String() string { } // GenerateFunc takes a context and returns a result, performing extension generate behaviors. -type GenerateFunc func(context GenerateContext) (GenerateResult, error) +type GenerateFunc[PL any, EM any] func(context GenerateContext[PL, EM]) (GenerateResult, error) // Generate is called by the main function of a extension, for generate phase -func Generate(generate GenerateFunc, config Config) { +func Generate[PL any, EM any](generate GenerateFunc[PL, EM], config Config) { var ( err error file string ok bool ) - ctx := GenerateContext{Logger: config.logger} + ctx := GenerateContext[PL, EM]{Logger: config.logger} ctx.ApplicationPath, err = os.Getwd() if err != nil { diff --git a/generate_test.go b/generate_test.go index 0cbedcd..82dc940 100644 --- a/generate_test.go +++ b/generate_test.go @@ -38,7 +38,7 @@ func testGenerate(t *testing.T, context spec.G, it spec.S) { var ( Expect = NewWithT(t).Expect - generateFunc libcnb.GenerateFunc + generateFunc libcnb.GenerateFunc[string, string] applicationPath string extensionPath string outputPath string @@ -55,7 +55,7 @@ func testGenerate(t *testing.T, context spec.G, it spec.S) { ) it.Before(func() { - generateFunc = func(libcnb.GenerateContext) (libcnb.GenerateResult, error) { + generateFunc = func(libcnb.GenerateContext[string, string]) (libcnb.GenerateResult, error) { return libcnb.NewGenerateResult(), nil } @@ -248,7 +248,7 @@ version = "1.1.1" }) context("has a build environment", func() { - var ctx libcnb.GenerateContext + var ctx libcnb.GenerateContext[string, string] it.Before(func() { Expect(os.WriteFile(filepath.Join(extensionPath, "extension.toml"), @@ -263,7 +263,7 @@ version = "1.1.1" 0600), ).To(Succeed()) - generateFunc = func(context libcnb.GenerateContext) (libcnb.GenerateResult, error) { + generateFunc = func(context libcnb.GenerateContext[string, string]) (libcnb.GenerateResult, error) { ctx = context return libcnb.NewGenerateResult(), nil } @@ -275,7 +275,7 @@ version = "1.1.1" libcnb.WithArguments([]string{commandPath})), ) Expect(ctx.ApplicationPath).To(Equal(applicationPath)) - Expect(ctx.Extension).To(Equal(libcnb.Extension{ + Expect(ctx.Extension).To(Equal(libcnb.Extension[string]{ API: "0.8", Info: libcnb.ExtensionInfo{ ID: "test-id", @@ -285,11 +285,11 @@ version = "1.1.1" Path: extensionPath, })) Expect(ctx.OutputDirectory).To(Equal(outputPath)) - Expect(ctx.Plan).To(Equal(libcnb.BuildpackPlan{ - Entries: []libcnb.BuildpackPlanEntry{ + Expect(ctx.Plan).To(Equal(libcnb.BuildpackPlan[string]{ + Entries: []libcnb.BuildpackPlanEntry[string]{ { Name: "test-name", - Metadata: map[string]interface{}{ + Metadata: map[string]string{ "test-key": "test-value", }, }, @@ -313,7 +313,7 @@ version = "1.1.1" }) context("has a build environment specifying target metadata", func() { - var ctx libcnb.GenerateContext + var ctx libcnb.GenerateContext[string, string] it.Before(func() { Expect(os.WriteFile(filepath.Join(extensionPath, "extension.toml"), @@ -343,7 +343,7 @@ version = "1.1.1" `), 0600), ).To(Succeed()) - generateFunc = func(context libcnb.GenerateContext) (libcnb.GenerateResult, error) { + generateFunc = func(context libcnb.GenerateContext[string, string]) (libcnb.GenerateResult, error) { ctx = context return libcnb.NewGenerateResult(), nil } @@ -389,7 +389,7 @@ version = "1.1.1" }) it("handles error from GenerateFunc", func() { - generateFunc = func(libcnb.GenerateContext) (libcnb.GenerateResult, error) { + generateFunc = func(libcnb.GenerateContext[string, string]) (libcnb.GenerateResult, error) { return libcnb.NewGenerateResult(), errors.New("test-error") } @@ -404,7 +404,7 @@ version = "1.1.1" }) it("writes Dockerfiles", func() { - generateFunc = func(_ libcnb.GenerateContext) (libcnb.GenerateResult, error) { + generateFunc = func(_ libcnb.GenerateContext[string, string]) (libcnb.GenerateResult, error) { result := libcnb.NewGenerateResult() result.BuildDockerfile = []byte(`FROM foo:latest`) result.RunDockerfile = []byte(`FROM bar:latest`) @@ -423,7 +423,7 @@ version = "1.1.1" }) it("writes extend-config.toml", func() { - generateFunc = func(_ libcnb.GenerateContext) (libcnb.GenerateResult, error) { + generateFunc = func(_ libcnb.GenerateContext[string, string]) (libcnb.GenerateResult, error) { result := libcnb.NewGenerateResult() result.Config = &libcnb.ExtendConfig{ Build: libcnb.BuildConfig{ diff --git a/layer.go b/layer.go index 7f14fac..4eb2d18 100644 --- a/layer.go +++ b/layer.go @@ -90,12 +90,12 @@ func SBOMFormatFromString(from string) (SBOMFormat, error) { } // Contribute represents a layer managed by the buildpack. -type Layer struct { +type Layer[LM any] struct { // LayerTypes indicates the type of layer LayerTypes `toml:"types"` // Metadata is the metadata associated with the layer. - Metadata map[string]interface{} `toml:"metadata"` + Metadata map[string]LM `toml:"metadata"` // Name is the name of the layer. Name string `toml:"-"` @@ -116,7 +116,7 @@ type Layer struct { Exec Exec `toml:"-"` } -func (l Layer) Reset() (Layer, error) { +func (l Layer[LM]) Reset() (Layer[LM], error) { l.LayerTypes = LayerTypes{ Build: false, Launch: false, @@ -130,19 +130,19 @@ func (l Layer) Reset() (Layer, error) { err := os.RemoveAll(l.Path) if err != nil { - return Layer{}, fmt.Errorf("error could not remove file: %s", err) + return Layer[LM]{}, fmt.Errorf("error could not remove file: %s", err) } err = os.MkdirAll(l.Path, os.ModePerm) if err != nil { - return Layer{}, fmt.Errorf("error could not create directory: %s", err) + return Layer[LM]{}, fmt.Errorf("error could not create directory: %s", err) } return l, nil } // SBOMPath returns the path to the layer specific SBOM File -func (l Layer) SBOMPath(bt SBOMFormat) string { +func (l Layer[LM]) SBOMPath(bt SBOMFormat) string { return filepath.Join(filepath.Dir(l.Path), fmt.Sprintf("%s.sbom.%s", l.Name, bt)) } @@ -160,14 +160,14 @@ type LayerTypes struct { } // Layers represents the layers part of the specification. -type Layers struct { +type Layers[LM any] struct { // Path is the layers filesystem location. Path string } // Layer creates a new layer, loading metadata if it exists. -func (l *Layers) Layer(name string) (Layer, error) { - layer := Layer{ +func (l *Layers[LM]) Layer(name string) (Layer[LM], error) { + layer := Layer[LM]{ Name: name, Path: filepath.Join(l.Path, name), BuildEnvironment: Environment{}, @@ -178,18 +178,18 @@ func (l *Layers) Layer(name string) (Layer, error) { f := filepath.Join(l.Path, fmt.Sprintf("%s.toml", name)) if _, err := toml.DecodeFile(f, &layer); err != nil && !os.IsNotExist(err) { - return Layer{}, fmt.Errorf("unable to decode layer metadata %s\n%w", f, err) + return Layer[LM]{}, fmt.Errorf("unable to decode layer metadata %s\n%w", f, err) } return layer, nil } // BOMBuildPath returns the full path to the build SBoM file for the buildpack -func (l Layers) BuildSBOMPath(bt SBOMFormat) string { +func (l Layers[LM]) BuildSBOMPath(bt SBOMFormat) string { return filepath.Join(l.Path, fmt.Sprintf("build.sbom.%s", bt)) } // BOMLaunchPath returns the full path to the launch SBoM file for the buildpack -func (l Layers) LaunchSBOMPath(bt SBOMFormat) string { +func (l Layers[LM]) LaunchSBOMPath(bt SBOMFormat) string { return filepath.Join(l.Path, fmt.Sprintf("launch.sbom.%s", bt)) } diff --git a/layer_test.go b/layer_test.go index 46c3637..cc4d1ea 100644 --- a/layer_test.go +++ b/layer_test.go @@ -31,7 +31,7 @@ func testLayer(t *testing.T, context spec.G, it spec.S) { var ( Expect = NewWithT(t).Expect - layers libcnb.Layers + layers libcnb.Layers[string] path string ) @@ -53,15 +53,15 @@ func testLayer(t *testing.T, context spec.G, it spec.S) { }) context("Reset", func() { - var layer libcnb.Layer + var layer libcnb.Layer[string] it.Before(func() { - layers = libcnb.Layers{Path: t.TempDir()} + layers = libcnb.Layers[string]{Path: t.TempDir()} }) context("when there is no previous build", func() { it.Before(func() { - layer = libcnb.Layer{ + layer = libcnb.Layer[string]{ Name: "test-name", Path: filepath.Join(layers.Path, "test-name"), LayerTypes: libcnb.LayerTypes{ @@ -77,7 +77,7 @@ func testLayer(t *testing.T, context spec.G, it spec.S) { layer, err = layer.Reset() Expect(err).NotTo(HaveOccurred()) - Expect(layer).To(Equal(libcnb.Layer{ + Expect(layer).To(Equal(libcnb.Layer[string]{ Name: "test-name", Path: filepath.Join(layers.Path, "test-name"), LayerTypes: libcnb.LayerTypes{ @@ -120,7 +120,7 @@ func testLayer(t *testing.T, context spec.G, it spec.S) { err = os.WriteFile(filepath.Join(launchEnvDir, "APPEND_VAR.delim"), []byte("!"), 0600) Expect(err).NotTo(HaveOccurred()) - layer = libcnb.Layer{ + layer = libcnb.Layer[string]{ Name: "test-name", Path: filepath.Join(layers.Path, "test-name"), LayerTypes: libcnb.LayerTypes{ @@ -138,7 +138,7 @@ func testLayer(t *testing.T, context spec.G, it spec.S) { "APPEND_VAR.append": "append-value", "APPEND_VAR.delim": "!", }, - Metadata: map[string]interface{}{ + Metadata: map[string]string{ "some-key": "some-value", }, } @@ -149,7 +149,7 @@ func testLayer(t *testing.T, context spec.G, it spec.S) { layer, err := layer.Reset() Expect(err).NotTo(HaveOccurred()) - Expect(layer).To(Equal(libcnb.Layer{ + Expect(layer).To(Equal(libcnb.Layer[string]{ Name: "test-name", Path: filepath.Join(layers.Path, "test-name"), LayerTypes: libcnb.LayerTypes{ @@ -182,7 +182,7 @@ func testLayer(t *testing.T, context spec.G, it spec.S) { }) it("return an error", func() { - layer := libcnb.Layer{ + layer := libcnb.Layer[string]{ Name: "some-layer", Path: filepath.Join(layers.Path, "some-layer"), } @@ -200,7 +200,7 @@ func testLayer(t *testing.T, context spec.G, it spec.S) { path, err = os.MkdirTemp("", "layers") Expect(err).NotTo(HaveOccurred()) - layers = libcnb.Layers{Path: path} + layers = libcnb.Layers[string]{Path: path} }) it.After(func() { @@ -269,7 +269,7 @@ test-key = "test-value" l, err := layers.Layer("test-name") Expect(err).NotTo(HaveOccurred()) - Expect(l.Metadata).To(Equal(map[string]interface{}{"test-key": "test-value"})) + Expect(l.Metadata).To(Equal(map[string]string{"test-key": "test-value"})) Expect(l.Launch).To(BeTrue()) Expect(l.Build).To(BeFalse()) }) @@ -292,7 +292,7 @@ test-key = "test-value" l, err := layers.Layer("test-name") Expect(err).NotTo(HaveOccurred()) - Expect(l.Metadata).To(Equal(map[string]interface{}{"test-key": "test-value"})) + Expect(l.Metadata).To(Equal(map[string]string{"test-key": "test-value"})) Expect(l.Launch).To(BeFalse()) Expect(l.Build).To(BeFalse()) Expect(l.Cache).To(BeFalse()) diff --git a/main.go b/main.go index fe2f8b8..2de02d2 100644 --- a/main.go +++ b/main.go @@ -21,7 +21,7 @@ import ( "path/filepath" ) -func main(detect DetectFunc, build BuildFunc, generate GenerateFunc, options ...Option) { +func main[DPL any, BPL any, PM any, LM any, EM any, BM any](detect DetectFunc[DPL, EM, BM], build BuildFunc[BPL, PM, LM, BM], generate GenerateFunc[BPL, EM], options ...Option) { config := NewConfig(options...) if len(config.arguments) == 0 { @@ -45,11 +45,11 @@ func main(detect DetectFunc, build BuildFunc, generate GenerateFunc, options ... } // BuildpackMain is called by the main function of a buildpack, encapsulating both detection and build in the same binary. -func BuildpackMain(detect DetectFunc, build BuildFunc, options ...Option) { +func BuildpackMain[DPL any, BPL any, PM any, LM any, EM any, BM any](detect DetectFunc[DPL, EM, BM], build BuildFunc[BPL, PM, LM, BM], options ...Option) { main(detect, build, nil, options...) } // ExtensionMain is called by the main function of a extension, encapsulating both detection and generation in the same binary. -func ExtensionMain(detect DetectFunc, generate GenerateFunc, options ...Option) { - main(detect, nil, generate, options...) +func ExtensionMain[DPL any, BPL any, PM any, LM any, EM any, BM any](detect DetectFunc[DPL, EM, BM], generate GenerateFunc[BPL, EM], options ...Option) { + main(detect, EmptyBuildFunc[BPL, PM, LM], generate, options...) } diff --git a/main_test.go b/main_test.go index d0b20f3..f13d003 100644 --- a/main_test.go +++ b/main_test.go @@ -35,13 +35,13 @@ func testMain(t *testing.T, _ spec.G, it spec.S) { Expect = NewWithT(t).Expect applicationPath string - buildFunc libcnb.BuildFunc + buildFunc libcnb.BuildFunc[string, string, string, string] buildpackPath string buildpackPlanPath string - detectFunc libcnb.DetectFunc + detectFunc libcnb.DetectFunc[string, string, string] environmentWriter *mocks.EnvironmentWriter exitHandler *mocks.ExitHandler - generateFunc libcnb.GenerateFunc + generateFunc libcnb.GenerateFunc[string, string] layersPath string platformPath string tomlWriter *mocks.TOMLWriter @@ -57,9 +57,7 @@ func testMain(t *testing.T, _ spec.G, it spec.S) { applicationPath, err = filepath.EvalSymlinks(applicationPath) Expect(err).NotTo(HaveOccurred()) - buildFunc = func(libcnb.BuildContext) (libcnb.BuildResult, error) { - return libcnb.NewBuildResult(), nil - } + buildFunc = libcnb.EmptyBuildFunc[string, string] buildpackPath, err = os.MkdirTemp("", "main-buildpack-path") Expect(err).NotTo(HaveOccurred()) @@ -107,11 +105,11 @@ test-key = "test-value" 0600), ).To(Succeed()) - detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { - return libcnb.DetectResult{}, nil + detectFunc = func(libcnb.DetectContext[string, string]) (libcnb.DetectResult[string], error) { + return libcnb.DetectResult[string]{}, nil } - generateFunc = func(libcnb.GenerateContext) (libcnb.GenerateResult, error) { + generateFunc = func(libcnb.GenerateContext[string, string]) (libcnb.GenerateResult, error) { return libcnb.GenerateResult{}, nil } @@ -207,8 +205,8 @@ test-key = "test-value" }) it("calls detector for detect command", func() { - detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { - return libcnb.DetectResult{Pass: true}, nil + detectFunc = func(libcnb.DetectContext[string, string]) (libcnb.DetectResult[string], error) { + return libcnb.DetectResult[string]{Pass: true}, nil } commandPath := filepath.Join("bin", "detect") @@ -220,12 +218,12 @@ test-key = "test-value" }) it("calls generator for generate command", func() { - generateFunc = func(libcnb.GenerateContext) (libcnb.GenerateResult, error) { + generateFunc = func(libcnb.GenerateContext[string, string]) (libcnb.GenerateResult, error) { return libcnb.GenerateResult{}, nil } commandPath := filepath.Join("bin", "generate") - libcnb.ExtensionMain(nil, generateFunc, + libcnb.ExtensionMain[string, string, string, string, string, string](nil, generateFunc, libcnb.WithArguments([]string{commandPath}), libcnb.WithExitHandler(exitHandler), libcnb.WithLogger(log.NewDiscard()), @@ -233,8 +231,8 @@ test-key = "test-value" }) it("calls exitHandler.Pass() on detection pass", func() { - detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { - return libcnb.DetectResult{Pass: true}, nil + detectFunc = func(libcnb.DetectContext[string, string]) (libcnb.DetectResult[string], error) { + return libcnb.DetectResult[string]{Pass: true}, nil } commandPath := filepath.Join("bin", "detect") @@ -248,8 +246,8 @@ test-key = "test-value" }) it("calls exitHandler.Fail() on detection fail", func() { - detectFunc = func(libcnb.DetectContext) (libcnb.DetectResult, error) { - return libcnb.DetectResult{Pass: false}, nil + detectFunc = func(libcnb.DetectContext[string, string]) (libcnb.DetectResult[string], error) { + return libcnb.DetectResult[string]{Pass: false}, nil } commandPath := filepath.Join("bin", "detect") diff --git a/store.go b/store.go index f2815c3..16fa292 100644 --- a/store.go +++ b/store.go @@ -17,8 +17,8 @@ package libcnb // Store represents the contents of store.toml -type Store struct { +type Store[PM any] struct { // Metadata represents the persistent metadata. - Metadata map[string]interface{} `toml:"metadata"` + Metadata map[string]PM `toml:"metadata"` }