From 7cac376a8bb782f330217740d4f5f403be530c72 Mon Sep 17 00:00:00 2001 From: William Perreault Date: Wed, 4 Sep 2024 18:13:12 -0400 Subject: [PATCH] Rename spec* to unit (#21) --- pkg/specter/artifactproc.go | 20 +- pkg/specter/artifactproc_fileartifact.go | 2 +- pkg/specter/assembly.go | 36 +- pkg/specter/assembly_test.go | 36 +- pkg/specter/common.go | 2 +- pkg/specter/pipeline.go | 72 +-- pkg/specter/pipeline_test.go | 42 +- pkg/specter/specloading.go | 155 ------- pkg/specter/specloading_test.go | 425 ------------------ pkg/specter/specter_test.go | 28 +- pkg/specter/srcloading.go | 2 +- pkg/specter/unitloading.go | 155 +++++++ pkg/specter/unitloading_test.go | 425 ++++++++++++++++++ pkg/specter/{specproc.go => unitproc.go} | 16 +- .../{specproc_test.go => unitproc_test.go} | 0 pkg/specterutils/README.md | 12 +- pkg/specterutils/depresolve.go | 56 +-- pkg/specterutils/depresolve_test.go | 132 +++--- .../{genericspec.go => genericunit.go} | 52 +-- ...enericspec_test.go => genericunit_test.go} | 72 +-- pkg/specterutils/hcl.go | 78 ++-- pkg/specterutils/hcl_test.go | 94 ++-- pkg/specterutils/linting.go | 108 ++--- pkg/specterutils/linting_test.go | 198 ++++---- pkg/specterutils/specversion_test.go | 139 ------ .../{specversion.go => unitversion.go} | 22 +- pkg/specterutils/unitversion_test.go | 139 ++++++ 27 files changed, 1266 insertions(+), 1252 deletions(-) delete mode 100644 pkg/specter/specloading.go delete mode 100644 pkg/specter/specloading_test.go create mode 100644 pkg/specter/unitloading.go create mode 100644 pkg/specter/unitloading_test.go rename pkg/specter/{specproc.go => unitproc.go} (79%) rename pkg/specter/{specproc_test.go => unitproc_test.go} (100%) rename pkg/specterutils/{genericspec.go => genericunit.go} (59%) rename pkg/specterutils/{genericspec_test.go => genericunit_test.go} (57%) delete mode 100644 pkg/specterutils/specversion_test.go rename pkg/specterutils/{specversion.go => unitversion.go} (64%) create mode 100644 pkg/specterutils/unitversion_test.go diff --git a/pkg/specter/artifactproc.go b/pkg/specter/artifactproc.go index 6d8c18d..014e593 100644 --- a/pkg/specter/artifactproc.go +++ b/pkg/specter/artifactproc.go @@ -1,3 +1,17 @@ +// Copyright 2024 Morébec +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package specter import ( @@ -81,9 +95,9 @@ func (n ProcessorArtifactRegistry) FindAll() ([]ArtifactRegistryEntry, error) { type ArtifactProcessingContext struct { context.Context - Specifications SpecificationGroup - Artifacts []Artifact - Logger Logger + Units UnitGroup + Artifacts []Artifact + Logger Logger ArtifactRegistry ProcessorArtifactRegistry processorName string diff --git a/pkg/specter/artifactproc_fileartifact.go b/pkg/specter/artifactproc_fileartifact.go index 60afa41..c253e78 100644 --- a/pkg/specter/artifactproc_fileartifact.go +++ b/pkg/specter/artifactproc_fileartifact.go @@ -39,7 +39,7 @@ const DefaultWriteMode WriteMode = WriteOnceMode var _ Artifact = (*FileArtifact)(nil) -// FileArtifact is a data structure that can be used by a SpecificationProcessor to generate file artifacts +// FileArtifact is a data structure that can be used by a UnitProcessor to generate file artifacts // that can be written by the FileArtifactProcessor. type FileArtifact struct { Path string diff --git a/pkg/specter/assembly.go b/pkg/specter/assembly.go index 795e90a..718c556 100644 --- a/pkg/specter/assembly.go +++ b/pkg/specter/assembly.go @@ -35,50 +35,50 @@ type PipelineOption func(s *Pipeline) // WithLogger configures the Logger of a Pipeline instance. func WithLogger(l Logger) PipelineOption { - return func(s *Pipeline) { - s.Logger = l + return func(p *Pipeline) { + p.Logger = l } } // WithSourceLoaders configures the SourceLoader of a Pipeline instance. func WithSourceLoaders(loaders ...SourceLoader) PipelineOption { - return func(s *Pipeline) { - s.SourceLoaders = append(s.SourceLoaders, loaders...) + return func(p *Pipeline) { + p.SourceLoaders = append(p.SourceLoaders, loaders...) } } -// WithLoaders configures the SpecificationLoader of a Pipeline instance. -func WithLoaders(loaders ...SpecificationLoader) PipelineOption { - return func(s *Pipeline) { - s.Loaders = append(s.Loaders, loaders...) +// WithLoaders configures the UnitLoader of a Pipeline instance. +func WithLoaders(loaders ...UnitLoader) PipelineOption { + return func(p *Pipeline) { + p.Loaders = append(p.Loaders, loaders...) } } -// WithProcessors configures the SpecProcess of a Pipeline instance. -func WithProcessors(processors ...SpecificationProcessor) PipelineOption { - return func(s *Pipeline) { - s.Processors = append(s.Processors, processors...) +// WithProcessors configures the UnitProcess of a Pipeline instance. +func WithProcessors(processors ...UnitProcessor) PipelineOption { + return func(p *Pipeline) { + p.Processors = append(p.Processors, processors...) } } // WithArtifactProcessors configures the ArtifactProcessor of a Pipeline instance. func WithArtifactProcessors(processors ...ArtifactProcessor) PipelineOption { - return func(s *Pipeline) { - s.ArtifactProcessors = append(s.ArtifactProcessors, processors...) + return func(p *Pipeline) { + p.ArtifactProcessors = append(p.ArtifactProcessors, processors...) } } // WithTimeProvider configures the TimeProvider of a Pipeline instance. func WithTimeProvider(tp TimeProvider) PipelineOption { - return func(s *Pipeline) { - s.TimeProvider = tp + return func(p *Pipeline) { + p.TimeProvider = tp } } // WithArtifactRegistry configures the ArtifactRegistry of a Pipeline instance. func WithArtifactRegistry(r ArtifactRegistry) PipelineOption { - return func(s *Pipeline) { - s.ArtifactRegistry = r + return func(p *Pipeline) { + p.ArtifactRegistry = r } } diff --git a/pkg/specter/assembly_test.go b/pkg/specter/assembly_test.go index 940a802..a80fe42 100644 --- a/pkg/specter/assembly_test.go +++ b/pkg/specter/assembly_test.go @@ -23,53 +23,53 @@ import ( ) func TestWithDefaultLogger(t *testing.T) { - s := NewPipeline(WithDefaultLogger()) - assert.IsType(t, &DefaultLogger{}, s.Logger) + p := NewPipeline(WithDefaultLogger()) + assert.IsType(t, &DefaultLogger{}, p.Logger) } func TestWithSourceLoaders(t *testing.T) { loader := &FileSystemSourceLoader{} - s := NewPipeline(WithSourceLoaders(loader)) - require.Contains(t, s.SourceLoaders, loader) + p := NewPipeline(WithSourceLoaders(loader)) + require.Contains(t, p.SourceLoaders, loader) } func TestWithLoaders(t *testing.T) { - loader := &specterutils.HCLGenericSpecLoader{} - s := NewPipeline(WithLoaders(loader)) - require.Contains(t, s.Loaders, loader) + loader := &specterutils.HCLGenericUnitLoader{} + p := NewPipeline(WithLoaders(loader)) + require.Contains(t, p.Loaders, loader) } func TestWithProcessors(t *testing.T) { processor := specterutils.LintingProcessor{} - s := NewPipeline(WithProcessors(processor)) - require.Contains(t, s.Processors, processor) + p := NewPipeline(WithProcessors(processor)) + require.Contains(t, p.Processors, processor) } func TestWithArtifactProcessors(t *testing.T) { processor := FileArtifactProcessor{} - s := NewPipeline(WithArtifactProcessors(processor)) - require.Contains(t, s.ArtifactProcessors, processor) + p := NewPipeline(WithArtifactProcessors(processor)) + require.Contains(t, p.ArtifactProcessors, processor) } func TestWithTimeProvider(t *testing.T) { tp := CurrentTimeProvider() - s := NewPipeline(WithTimeProvider(tp)) - require.NotNil(t, s.TimeProvider) + p := NewPipeline(WithTimeProvider(tp)) + require.NotNil(t, p.TimeProvider) } func TestWithArtifactRegistry(t *testing.T) { registry := &InMemoryArtifactRegistry{} - s := NewPipeline(WithArtifactRegistry(registry)) - require.Equal(t, s.ArtifactRegistry, registry) + p := NewPipeline(WithArtifactRegistry(registry)) + require.Equal(t, p.ArtifactRegistry, registry) } func TestWithJSONArtifactRegistry(t *testing.T) { fs := &mockFileSystem{} filePath := DefaultJSONArtifactRegistryFileName - s := NewPipeline(WithJSONArtifactRegistry(filePath, fs)) - require.IsType(t, &JSONArtifactRegistry{}, s.ArtifactRegistry) - registry := s.ArtifactRegistry.(*JSONArtifactRegistry) + p := NewPipeline(WithJSONArtifactRegistry(filePath, fs)) + require.IsType(t, &JSONArtifactRegistry{}, p.ArtifactRegistry) + registry := p.ArtifactRegistry.(*JSONArtifactRegistry) assert.Equal(t, registry.FileSystem, fs) assert.Equal(t, registry.FilePath, filePath) diff --git a/pkg/specter/common.go b/pkg/specter/common.go index 5f093e4..63c1f61 100644 --- a/pkg/specter/common.go +++ b/pkg/specter/common.go @@ -16,7 +16,7 @@ package specter type ArtifactID string -// Artifact represents a result or output generated by a SpecificationProcessor. +// Artifact represents a result or output generated by a UnitProcessor. // An artifact is a unit of data or information produced as part of the processing workflow. // It can be a transient, in-memory object, or it might represent more permanent entities such as // files on disk, records in a database, deployment units, or other forms of data artifacts. diff --git a/pkg/specter/pipeline.go b/pkg/specter/pipeline.go index 6916ae3..4feb668 100644 --- a/pkg/specter/pipeline.go +++ b/pkg/specter/pipeline.go @@ -32,15 +32,15 @@ const RunThrough RunMode = "run-through" const defaultRunMode = PreviewMode const SourceLoadingFailedErrorCode = "specter.source_loading_failed" -const SpecificationLoadingFailedErrorCode = "specter.specification_loading_failed" -const SpecificationProcessingFailedErrorCode = "specter.specification_processing_failed" +const UnitLoadingFailedErrorCode = "specter.unit_loading_failed" +const UnitProcessingFailedErrorCode = "specter.unit_processing_failed" const ArtifactProcessingFailedErrorCode = "specter.artifact_processing_failed" // Pipeline is the service responsible to run a specter pipeline. type Pipeline struct { SourceLoaders []SourceLoader - Loaders []SpecificationLoader - Processors []SpecificationProcessor + Loaders []UnitLoader + Processors []UnitProcessor ArtifactProcessors []ArtifactProcessor ArtifactRegistry ArtifactRegistry Logger Logger @@ -53,7 +53,7 @@ type PipelineResult struct { SourceLocations []string Sources []Source - Specifications []Specification + Units []Unit Artifacts []Artifact RunMode RunMode } @@ -92,18 +92,18 @@ func (p Pipeline) Run(ctx context.Context, sourceLocations []string, runMode Run return result, e } - // Load Specifications - result.Specifications, err = p.loadSpecifications(ctx, result.Sources) + // Load Units + result.Units, err = p.loadUnits(ctx, result.Sources) if err != nil { - e := errors.WrapWithMessage(err, SpecificationLoadingFailedErrorCode, "failed loading specifications") + e := errors.WrapWithMessage(err, UnitLoadingFailedErrorCode, "failed loading units") p.Logger.Error(e.Error()) return result, e } - // Process Specifications - result.Artifacts, err = p.processSpecifications(ctx, result.Specifications) + // Process Units + result.Artifacts, err = p.processUnits(ctx, result.Units) if err != nil { - e := errors.WrapWithMessage(err, SpecificationProcessingFailedErrorCode, "failed processing specifications") + e := errors.WrapWithMessage(err, UnitProcessingFailedErrorCode, "failed processing units") p.Logger.Error(e.Error()) return result, e } @@ -114,7 +114,7 @@ func (p Pipeline) Run(ctx context.Context, sourceLocations []string, runMode Run } // Process Artifact - if err = p.processArtifacts(ctx, result.Specifications, result.Artifacts); err != nil { + if err = p.processArtifacts(ctx, result.Units, result.Artifacts); err != nil { e := errors.WrapWithMessage(err, ArtifactProcessingFailedErrorCode, "failed processing artifacts") p.Logger.Error(e.Error()) return result, e @@ -131,7 +131,7 @@ func (p Pipeline) logResult(run PipelineResult) { p.Logger.Info(fmt.Sprintf("Run time: %s", run.ExecutionTime())) p.Logger.Info(fmt.Sprintf("Number of source locations: %d", len(run.SourceLocations))) p.Logger.Info(fmt.Sprintf("Number of sources: %d", len(run.Sources))) - p.Logger.Info(fmt.Sprintf("Number of specifications: %d", len(run.Specifications))) + p.Logger.Info(fmt.Sprintf("Number of units: %d", len(run.Units))) p.Logger.Info(fmt.Sprintf("Number of artifacts: %d", len(run.Artifacts))) } @@ -171,12 +171,12 @@ func (p Pipeline) loadSources(ctx context.Context, sourceLocations []string) ([] return sources, errors.GroupOrNil(errs) } -// loadSpecifications performs the loading of Specifications. -func (p Pipeline) loadSpecifications(ctx context.Context, sources []Source) ([]Specification, error) { - p.Logger.Info("\nLoading specifications ...") +// loadUnits performs the loading of Units. +func (p Pipeline) loadUnits(ctx context.Context, sources []Source) ([]Unit, error) { + p.Logger.Info("\nLoading units ...") - // Load specifications - var specifications []Specification + // Load units + var units []Unit var sourcesNotLoaded []Source errs := errors.NewGroup(errors.InternalErrorCode) @@ -190,14 +190,14 @@ func (p Pipeline) loadSpecifications(ctx context.Context, sources []Source) ([]S continue } - loadedSpecs, err := l.Load(src) + loadedUnits, err := l.Load(src) if err != nil { p.Logger.Error(err.Error()) errs = errs.Append(err) continue } - specifications = append(specifications, loadedSpecs...) + units = append(units, loadedUnits...) wasLoaded = true } @@ -211,24 +211,24 @@ func (p Pipeline) loadSpecifications(ctx context.Context, sources []Source) ([]S p.Logger.Warning(fmt.Sprintf("%q could not be loaded.", src.Location)) } - p.Logger.Warning("%d specifications were not loaded.") + p.Logger.Warning("%d units were not loaded.") } - p.Logger.Info(fmt.Sprintf("%d specifications loaded.", len(specifications))) + p.Logger.Info(fmt.Sprintf("%d units loaded.", len(units))) - return specifications, errors.GroupOrNil(errs) + return units, errors.GroupOrNil(errs) } -// processSpecifications sends the specifications to processors. -func (p Pipeline) processSpecifications(ctx context.Context, specs []Specification) ([]Artifact, error) { +// processUnits sends the units to processors. +func (p Pipeline) processUnits(ctx context.Context, units []Unit) ([]Artifact, error) { pctx := ProcessingContext{ - Context: ctx, - Specifications: specs, - Artifacts: nil, - Logger: p.Logger, + Context: ctx, + Units: units, + Artifacts: nil, + Logger: p.Logger, } - p.Logger.Info("\nProcessing specifications ...") + p.Logger.Info("\nProcessing units ...") for _, processor := range p.Processors { if err := ctx.Err(); err != nil { return nil, err @@ -245,12 +245,12 @@ func (p Pipeline) processSpecifications(ctx context.Context, specs []Specificati p.Logger.Info(fmt.Sprintf("-> %s", o.ID())) } - p.Logger.Success("Specifications processed successfully.") + p.Logger.Success("Units processed successfully.") return pctx.Artifacts, nil } // processArtifacts sends a list of ProcessingArtifacts to the registered ArtifactProcessors. -func (p Pipeline) processArtifacts(ctx context.Context, specifications []Specification, artifacts []Artifact) error { +func (p Pipeline) processArtifacts(ctx context.Context, units []Unit, artifacts []Artifact) error { if p.ArtifactRegistry == nil { p.ArtifactRegistry = &InMemoryArtifactRegistry{} } @@ -273,10 +273,10 @@ func (p Pipeline) processArtifacts(ctx context.Context, specifications []Specifi processorName := processor.Name() artifactCtx := ArtifactProcessingContext{ - Context: ctx, - Specifications: specifications, - Artifacts: artifacts, - Logger: p.Logger, + Context: ctx, + Units: units, + Artifacts: artifacts, + Logger: p.Logger, ArtifactRegistry: ProcessorArtifactRegistry{ processorName: processorName, registry: p.ArtifactRegistry, diff --git a/pkg/specter/pipeline_test.go b/pkg/specter/pipeline_test.go index 2a53069..f859154 100644 --- a/pkg/specter/pipeline_test.go +++ b/pkg/specter/pipeline_test.go @@ -31,7 +31,7 @@ func TestRunResult_ExecutionTime(t *testing.T) { require.Equal(t, r.ExecutionTime(), time.Hour*1) } -func TestSpecter_Run(t *testing.T) { +func TestUnitter_Run(t *testing.T) { testDay := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) type given struct { @@ -71,12 +71,12 @@ func TestSpecter_Run(t *testing.T) { }, then: then{ expectedRunResult: PipelineResult{ - RunMode: PreviewMode, - Sources: nil, - Specifications: nil, - Artifacts: nil, - StartedAt: testDay, - EndedAt: testDay, + RunMode: PreviewMode, + Sources: nil, + Units: nil, + Artifacts: nil, + StartedAt: testDay, + EndedAt: testDay, }, expectedError: assert.NoError, }, @@ -97,12 +97,12 @@ func TestSpecter_Run(t *testing.T) { }, then: then{ expectedRunResult: PipelineResult{ - RunMode: PreviewMode, - Sources: nil, - Specifications: nil, - Artifacts: nil, - StartedAt: testDay, - EndedAt: testDay, + RunMode: PreviewMode, + Sources: nil, + Units: nil, + Artifacts: nil, + StartedAt: testDay, + EndedAt: testDay, }, expectedError: assert.NoError, }, @@ -123,12 +123,12 @@ func TestSpecter_Run(t *testing.T) { }, then: then{ expectedRunResult: PipelineResult{ - RunMode: PreviewMode, - Sources: nil, - Specifications: nil, - Artifacts: nil, - StartedAt: testDay, - EndedAt: testDay, + RunMode: PreviewMode, + Sources: nil, + Units: nil, + Artifacts: nil, + StartedAt: testDay, + EndedAt: testDay, }, expectedError: assert.NoError, }, @@ -136,9 +136,9 @@ func TestSpecter_Run(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - s := tt.given.pipeline() + p := tt.given.pipeline() - actualResult, err := s.Run(tt.when.context, tt.when.sourceLocations, tt.when.executionMode) + actualResult, err := p.Run(tt.when.context, tt.when.sourceLocations, tt.when.executionMode) if tt.then.expectedError != nil { tt.then.expectedError(t, err) } else { diff --git a/pkg/specter/specloading.go b/pkg/specter/specloading.go deleted file mode 100644 index 5a6be93..0000000 --- a/pkg/specter/specloading.go +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright 2024 Morébec -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package specter - -// UnsupportedSourceErrorCode ErrorSeverity code returned by a SpecificationLoader when a given loader does not support a certain source. -const UnsupportedSourceErrorCode = "specter.spec_loading.unsupported_source" - -// SpecificationLoader is a service responsible for loading Specifications from Sources. -type SpecificationLoader interface { - // Load loads a slice of Specification from a Source, or returns an error if it encountered a failure. - Load(s Source) ([]Specification, error) - - // SupportsSource indicates if this loader supports a certain source or not. - SupportsSource(s Source) bool -} - -type SpecificationType string - -type SpecificationName string - -// Specification is a general purpose data structure to represent a specification as loaded from a file regardless of the loader -// used. -// It is the responsibility of the application using specter to convert a specification to an appropriate data structure representing the intent of a -// given Specification. -type Specification interface { - // Name returns the unique Name of this specification. - Name() SpecificationName - - // Type returns the type of this specification. - Type() SpecificationType - - // Description of this specification. - Description() string - - // Source returns the source of this specification. - Source() Source - - // SetSource sets the source of the specification. - // This method should only be used by loaders. - SetSource(s Source) -} - -// SpecificationGroup Represents a list of Specification. -type SpecificationGroup []Specification - -func NewSpecGroup(s ...Specification) SpecificationGroup { - g := SpecificationGroup{} - return append(g, s...) -} - -// Merge Allows merging a group with another one. -func (g SpecificationGroup) Merge(group SpecificationGroup) SpecificationGroup { - merged := g - typeNameIndex := map[SpecificationName]any{} - for _, s := range g { - typeNameIndex[s.Name()] = nil - } - for _, s := range group { - if _, found := typeNameIndex[s.Name()]; found { - continue - } - typeNameIndex[s.Name()] = nil - merged = append(merged, s) - } - return merged -} - -// Select allows filtering the group for certain specifications. -func (g SpecificationGroup) Select(p func(s Specification) bool) SpecificationGroup { - r := SpecificationGroup{} - for _, s := range g { - if p(s) { - r = append(r, s) - } - } - - return r -} - -func (g SpecificationGroup) SelectType(t SpecificationType) SpecificationGroup { - return g.Select(func(s Specification) bool { - return s.Type() == t - }) -} - -func (g SpecificationGroup) SelectName(t SpecificationName) Specification { - for _, s := range g { - if s.Name() == t { - return s - } - } - - return nil -} - -func (g SpecificationGroup) SelectNames(names ...SpecificationName) SpecificationGroup { - return g.Select(func(s Specification) bool { - for _, name := range names { - if s.Name() == name { - return true - } - } - return false - }) -} - -func (g SpecificationGroup) Exclude(p func(s Specification) bool) SpecificationGroup { - r := SpecificationGroup{} - for _, s := range g { - if !p(s) { - r = append(r, s) - } - } - - return r -} - -func (g SpecificationGroup) ExcludeType(t SpecificationType) SpecificationGroup { - return g.Exclude(func(s Specification) bool { - return s.Type() == t - }) -} - -func (g SpecificationGroup) ExcludeNames(names ...SpecificationName) SpecificationGroup { - return g.Exclude(func(s Specification) bool { - for _, name := range names { - if s.Name() == name { - return true - } - } - return false - }) -} - -// MapSpecGroup performs a map operation on a SpecificationGroup -func MapSpecGroup[T any](g SpecificationGroup, p func(s Specification) T) []T { - var mapped []T - for _, s := range g { - mapped = append(mapped, p(s)) - } - - return mapped -} diff --git a/pkg/specter/specloading_test.go b/pkg/specter/specloading_test.go deleted file mode 100644 index 6ac1a1e..0000000 --- a/pkg/specter/specloading_test.go +++ /dev/null @@ -1,425 +0,0 @@ -// Copyright 2024 Morébec -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package specter_test - -import ( - . "github.com/morebec/specter/pkg/specter" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "testing" -) - -// Test cases for NewSpecGroup -func TestNewSpecGroup(t *testing.T) { - tests := []struct { - name string - given []Specification - when func() SpecificationGroup - then func(SpecificationGroup) bool - }{ - { - name: "GIVEN no specifications WHEN calling NewSpecGroup THEN return an empty group", - given: []Specification{}, - when: func() SpecificationGroup { - return NewSpecGroup() - }, - then: func(result SpecificationGroup) bool { - return len(result) == 0 - }, - }, - { - name: "GIVEN multiple specifications WHEN calling NewSpecGroup THEN return a group with those specifications", - given: []Specification{ - &SpecificationStub{name: "spec1", typeName: "type", source: Source{}}, - &SpecificationStub{name: "spec2", typeName: "type", source: Source{}}, - }, - when: func() SpecificationGroup { - return NewSpecGroup( - &SpecificationStub{name: "spec1", typeName: "type", source: Source{}}, - &SpecificationStub{name: "spec2", typeName: "type", source: Source{}}, - ) - }, - then: func(result SpecificationGroup) bool { - return len(result) == 2 && - result[0].Name() == "spec1" && - result[1].Name() == "spec2" - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := tt.when() - if !tt.then(result) { - t.Errorf("Test %s failed", tt.name) - } - }) - } -} - -// Test cases for merge -func TestSpecificationGroup_Merge(t *testing.T) { - tests := []struct { - name string - given SpecificationGroup - when SpecificationGroup - then SpecificationGroup - }{ - { - name: "GIVEN two disjoint groups THEN return a group with all specifications", - given: NewSpecGroup( - &SpecificationStub{name: "spec1", typeName: "type", source: Source{}}, - ), - when: NewSpecGroup( - &SpecificationStub{name: "spec2", typeName: "type", source: Source{}}, - ), - then: NewSpecGroup( - &SpecificationStub{name: "spec1", typeName: "type", source: Source{}}, - &SpecificationStub{name: "spec2", typeName: "type", source: Source{}}, - ), - }, - { - name: "GIVEN two groups with overlapping specifications THEN return a group without duplicates", - given: NewSpecGroup( - &SpecificationStub{name: "spec1", typeName: "type", source: Source{}}, - ), - when: NewSpecGroup( - &SpecificationStub{name: "spec1", typeName: "type", source: Source{}}, - &SpecificationStub{name: "spec2", typeName: "type", source: Source{}}, - ), - then: NewSpecGroup( - &SpecificationStub{name: "spec1", typeName: "type", source: Source{}}, - &SpecificationStub{name: "spec2", typeName: "type", source: Source{}}, - ), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := tt.given.Merge(tt.when) - require.Equal(t, tt.then, got) - }) - } -} - -func TestSpecificationGroup_Select(t *testing.T) { - tests := []struct { - name string - given SpecificationGroup - when func(s Specification) bool - then SpecificationGroup - }{ - { - name: "GIVEN no specifications matches, THEN return an empty group", - given: SpecificationGroup{ - &SpecificationStub{name: "spec2name", typeName: "type", source: Source{}}, - }, - when: func(s Specification) bool { - return false - }, - then: SpecificationGroup{}, - }, - { - name: "GIVEN specifications matches, THEN return a group with only matching specifications", - given: SpecificationGroup{ - &SpecificationStub{name: "spec1", typeName: "type", source: Source{}}, - &SpecificationStub{name: "spec2", typeName: "type", source: Source{}}, - }, - when: func(s Specification) bool { - return s.Name() == "spec2" - }, - then: SpecificationGroup{ - &SpecificationStub{name: "spec2", typeName: "type", source: Source{}}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := tt.given.Select(tt.when) - require.Equal(t, tt.then, got) - }) - } -} - -func TestSpecificationGroup_SelectType(t *testing.T) { - tests := []struct { - name string - given SpecificationGroup - when SpecificationType - then SpecificationGroup - }{ - { - name: "GIVEN no specifications matches, THEN return an empty group", - given: SpecificationGroup{ - &SpecificationStub{name: "spec2name", typeName: "type", source: Source{}}, - }, - when: "not_found", - then: SpecificationGroup{}, - }, - { - name: "GIVEN a specification matches, THEN return a group with matching specification", - given: SpecificationGroup{ - &SpecificationStub{name: "spec1", typeName: "type1", source: Source{}}, - &SpecificationStub{name: "spec2", typeName: "type2", source: Source{}}, - }, - when: "type1", - then: SpecificationGroup{ - &SpecificationStub{name: "spec1", typeName: "type1", source: Source{}}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := tt.given.SelectType(tt.when) - require.Equal(t, tt.then, got) - }) - } -} - -func TestSpecificationGroup_SelectName(t *testing.T) { - tests := []struct { - name string - given SpecificationGroup - when SpecificationName - then Specification - }{ - { - name: "GIVEN a group with multiple specifications WHEN selecting an existing name THEN return the corresponding specification", - given: NewSpecGroup( - &SpecificationStub{name: "spec1", typeName: "type", source: Source{}}, - &SpecificationStub{name: "spec2", typeName: "type", source: Source{}}, - ), - when: "spec2", - then: &SpecificationStub{name: "spec2", typeName: "type", source: Source{}}, - }, - { - name: "GIVEN a group with multiple specifications WHEN selecting a non-existent name THEN return nil", - given: NewSpecGroup( - &SpecificationStub{name: "spec1", typeName: "type", source: Source{}}, - &SpecificationStub{name: "spec2", typeName: "type", source: Source{}}, - ), - when: "spec3", - then: nil, - }, - { - name: "GIVEN an empty group WHEN selecting a name THEN return nil", - given: NewSpecGroup(), - when: "spec1", - then: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := tt.given.SelectName(tt.when) - require.Equal(t, tt.then, got) - }) - } -} - -func TestSpecificationGroup_SelectNames(t *testing.T) { - tests := []struct { - name string - given SpecificationGroup - when []SpecificationName - then SpecificationGroup - }{ - { - name: "GIVEN no specifications matches, THEN return a group with no values", - given: SpecificationGroup{ - &SpecificationStub{name: "name", typeName: "type", source: Source{}}, - }, - when: []SpecificationName{"not_found"}, - then: SpecificationGroup{}, - }, - { - name: "GIVEN a specification matches, THEN return a group with matching specification", - given: SpecificationGroup{ - &SpecificationStub{name: "spec1", typeName: "type", source: Source{}}, - &SpecificationStub{name: "spec2", typeName: "type", source: Source{}}, - }, - when: []SpecificationName{"spec1"}, - then: SpecificationGroup{ - &SpecificationStub{name: "spec1", typeName: "type", source: Source{}}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := tt.given.SelectNames(tt.when...) - require.Equal(t, tt.then, got) - }) - } -} - -func TestSpecificationGroup_Exclude(t *testing.T) { - tests := []struct { - name string - given SpecificationGroup - when func(s Specification) bool - then SpecificationGroup - }{ - { - name: "GIVEN no specifications matches, THEN return a group with the same values", - given: SpecificationGroup{ - &SpecificationStub{name: "name", typeName: "type", source: Source{}}, - }, - when: func(s Specification) bool { - return false - }, - then: SpecificationGroup{ - &SpecificationStub{name: "name", typeName: "type", source: Source{}}, - }, - }, - { - name: "GIVEN specifications matches, THEN return a group without matching specifications", - given: SpecificationGroup{ - &SpecificationStub{name: "spec1", typeName: "type", source: Source{}}, - &SpecificationStub{name: "spec2", typeName: "type", source: Source{}}, - }, - when: func(s Specification) bool { - return true - }, - then: SpecificationGroup{}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := tt.given.Exclude(tt.when) - require.Equal(t, tt.then, got) - }) - } -} - -func TestSpecificationGroup_ExcludeType(t *testing.T) { - tests := []struct { - name string - given SpecificationGroup - when SpecificationType - then SpecificationGroup - }{ - { - name: "GIVEN no specifications matches, THEN return a group with the same values", - given: SpecificationGroup{ - &SpecificationStub{name: "spec2name", typeName: "type", source: Source{}}, - }, - when: "not_found", - then: SpecificationGroup{ - &SpecificationStub{name: "spec2name", typeName: "type", source: Source{}}, - }, - }, - { - name: "GIVEN a specification matches, THEN return a group without matching specification", - given: SpecificationGroup{ - &SpecificationStub{name: "spec1", typeName: "type1", source: Source{}}, - &SpecificationStub{name: "spec2", typeName: "type2", source: Source{}}, - }, - when: "type1", - then: SpecificationGroup{ - &SpecificationStub{name: "spec2", typeName: "type2", source: Source{}}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := tt.given.ExcludeType(tt.when) - require.Equal(t, tt.then, got) - }) - } -} - -func TestSpecificationGroup_ExcludeNames(t *testing.T) { - tests := []struct { - name string - given SpecificationGroup - when []SpecificationName - then SpecificationGroup - }{ - { - name: "GIVEN no specifications matches, THEN return a group with the same values", - given: SpecificationGroup{ - &SpecificationStub{name: "spec2name", typeName: "type", source: Source{}}, - }, - when: []SpecificationName{"not_found"}, - then: SpecificationGroup{ - &SpecificationStub{name: "spec2name", typeName: "type", source: Source{}}, - }, - }, - { - name: "GIVEN a specification matches, THEN return a group without matching specification", - given: SpecificationGroup{ - &SpecificationStub{name: "spec1", typeName: "type", source: Source{}}, - &SpecificationStub{name: "spec2", typeName: "type", source: Source{}}, - }, - when: []SpecificationName{"spec1"}, - then: SpecificationGroup{ - &SpecificationStub{name: "spec2", typeName: "type", source: Source{}}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := tt.given.ExcludeNames(tt.when...) - require.Equal(t, tt.then, got) - }) - } -} - -func TestMapSpecGroup(t *testing.T) { - tests := []struct { - name string - given SpecificationGroup - when func(Specification) string - then []string - }{ - { - name: "GIVEN a group with multiple specifications WHEN mapped to their names THEN return a slice of specification names", - given: NewSpecGroup( - &SpecificationStub{name: "spec1", typeName: "type", source: Source{}}, - &SpecificationStub{name: "spec2", typeName: "type", source: Source{}}, - ), - when: func(s Specification) string { - return string(s.Name()) - }, - then: []string{"spec1", "spec2"}, - }, - { - name: "GIVEN an empty group WHEN mapped THEN return a nil slice", - given: NewSpecGroup(), - when: func(s Specification) string { - return string(s.Name()) - }, - then: nil, - }, - { - name: "GIVEN a group with multiple specifications WHEN mapped to a constant value THEN return a slice of that value", - given: NewSpecGroup( - &SpecificationStub{name: "spec1", typeName: "type", source: Source{}}, - &SpecificationStub{name: "spec2", typeName: "type", source: Source{}}, - ), - when: func(s Specification) string { - return "constant" - }, - then: []string{"constant", "constant"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := MapSpecGroup(tt.given, tt.when) - assert.Equal(t, tt.then, got) - }) - } -} diff --git a/pkg/specter/specter_test.go b/pkg/specter/specter_test.go index 7f23b34..17421ca 100644 --- a/pkg/specter/specter_test.go +++ b/pkg/specter/specter_test.go @@ -37,33 +37,33 @@ func RequireErrorWithCode(c string) require.ErrorAssertionFunc { } } -var _ specter.Specification = (*SpecificationStub)(nil) +var _ specter.Unit = (*UnitStub)(nil) -type SpecificationStub struct { - name specter.SpecificationName - typeName specter.SpecificationType +type UnitStub struct { + name specter.UnitName + typeName specter.UnitType source specter.Source desc string } -func (s *SpecificationStub) Name() specter.SpecificationName { - return s.name +func (us *UnitStub) Name() specter.UnitName { + return us.name } -func (s *SpecificationStub) Type() specter.SpecificationType { - return s.typeName +func (us *UnitStub) Type() specter.UnitType { + return us.typeName } -func (s *SpecificationStub) Description() string { - return s.desc +func (us *UnitStub) Description() string { + return us.desc } -func (s *SpecificationStub) Source() specter.Source { - return s.source +func (us *UnitStub) Source() specter.Source { + return us.source } -func (s *SpecificationStub) SetSource(src specter.Source) { - s.source = src +func (us *UnitStub) SetSource(src specter.Source) { + us.source = src } // FILE SYSTEM diff --git a/pkg/specter/srcloading.go b/pkg/specter/srcloading.go index a71e24b..c0e9105 100644 --- a/pkg/specter/srcloading.go +++ b/pkg/specter/srcloading.go @@ -26,7 +26,7 @@ import ( // SourceFormat represents the format or syntax of a source. type SourceFormat string -// Source represents the source code that was used to load a given specification. +// Source represents the source code that was used to load a given unit. type Source struct { // Location of the source, this can be a local file or a remote file. Location string diff --git a/pkg/specter/unitloading.go b/pkg/specter/unitloading.go new file mode 100644 index 0000000..84f18d8 --- /dev/null +++ b/pkg/specter/unitloading.go @@ -0,0 +1,155 @@ +// Copyright 2024 Morébec +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package specter + +// UnsupportedSourceErrorCode ErrorSeverity code returned by a UnitLoader when a given loader does not support a certain source. +const UnsupportedSourceErrorCode = "specter.spec_loading.unsupported_source" + +// UnitLoader is a service responsible for loading Units from Sources. +type UnitLoader interface { + // Load loads a slice of Unit from a Source, or returns an error if it encountered a failure. + Load(s Source) ([]Unit, error) + + // SupportsSource indicates if this loader supports a certain source or not. + SupportsSource(s Source) bool +} + +type UnitType string + +type UnitName string + +// Unit is a general purpose data structure to represent a unit as loaded from a file regardless of the loader +// used. +// It is the responsibility of the application using specter to convert a unit to an appropriate data structure representing the intent of a +// given Unit. +type Unit interface { + // Name returns the unique Name of this unit. + Name() UnitName + + // Type returns the type of this unit. + Type() UnitType + + // Description of this unit. + Description() string + + // Source returns the source of this unit. + Source() Source + + // SetSource sets the source of the unit. + // This method should only be used by loaders. + SetSource(s Source) +} + +// UnitGroup Represents a list of Unit. +type UnitGroup []Unit + +func NewUnitGroup(u ...Unit) UnitGroup { + g := UnitGroup{} + return append(g, u...) +} + +// Merge Allows merging a group with another one. +func (g UnitGroup) Merge(group UnitGroup) UnitGroup { + merged := g + typeNameIndex := map[UnitName]any{} + for _, u := range g { + typeNameIndex[u.Name()] = nil + } + for _, u := range group { + if _, found := typeNameIndex[u.Name()]; found { + continue + } + typeNameIndex[u.Name()] = nil + merged = append(merged, u) + } + return merged +} + +// Select allows filtering the group for certain units. +func (g UnitGroup) Select(p func(u Unit) bool) UnitGroup { + r := UnitGroup{} + for _, u := range g { + if p(u) { + r = append(r, u) + } + } + + return r +} + +func (g UnitGroup) SelectType(t UnitType) UnitGroup { + return g.Select(func(u Unit) bool { + return u.Type() == t + }) +} + +func (g UnitGroup) SelectName(t UnitName) Unit { + for _, u := range g { + if u.Name() == t { + return u + } + } + + return nil +} + +func (g UnitGroup) SelectNames(names ...UnitName) UnitGroup { + return g.Select(func(u Unit) bool { + for _, name := range names { + if u.Name() == name { + return true + } + } + return false + }) +} + +func (g UnitGroup) Exclude(p func(u Unit) bool) UnitGroup { + r := UnitGroup{} + for _, u := range g { + if !p(u) { + r = append(r, u) + } + } + + return r +} + +func (g UnitGroup) ExcludeType(t UnitType) UnitGroup { + return g.Exclude(func(u Unit) bool { + return u.Type() == t + }) +} + +func (g UnitGroup) ExcludeNames(names ...UnitName) UnitGroup { + return g.Exclude(func(u Unit) bool { + for _, name := range names { + if u.Name() == name { + return true + } + } + return false + }) +} + +// MapUnitGroup performs a map operation on a UnitGroup +func MapUnitGroup[T any](g UnitGroup, p func(u Unit) T) []T { + var mapped []T + for _, u := range g { + mapped = append(mapped, p(u)) + } + + return mapped +} diff --git a/pkg/specter/unitloading_test.go b/pkg/specter/unitloading_test.go new file mode 100644 index 0000000..395a4e7 --- /dev/null +++ b/pkg/specter/unitloading_test.go @@ -0,0 +1,425 @@ +// Copyright 2024 Morébec +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package specter_test + +import ( + . "github.com/morebec/specter/pkg/specter" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +// Test cases for NewUnitGroup +func TestNewUnitGroup(t *testing.T) { + tests := []struct { + name string + given []Unit + when func() UnitGroup + then func(UnitGroup) bool + }{ + { + name: "GIVEN no units WHEN calling NewUnitGroup THEN return an empty group", + given: []Unit{}, + when: func() UnitGroup { + return NewUnitGroup() + }, + then: func(result UnitGroup) bool { + return len(result) == 0 + }, + }, + { + name: "GIVEN multiple units WHEN calling NewUnitGroup THEN return a group with those units", + given: []Unit{ + &UnitStub{name: "unit1", typeName: "type", source: Source{}}, + &UnitStub{name: "unit2", typeName: "type", source: Source{}}, + }, + when: func() UnitGroup { + return NewUnitGroup( + &UnitStub{name: "unit1", typeName: "type", source: Source{}}, + &UnitStub{name: "unit2", typeName: "type", source: Source{}}, + ) + }, + then: func(result UnitGroup) bool { + return len(result) == 2 && + result[0].Name() == "unit1" && + result[1].Name() == "unit2" + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.when() + if !tt.then(result) { + t.Errorf("Test %s failed", tt.name) + } + }) + } +} + +// Test cases for merge +func TestUnitGroup_Merge(t *testing.T) { + tests := []struct { + name string + given UnitGroup + when UnitGroup + then UnitGroup + }{ + { + name: "GIVEN two disjoint groups THEN return a group with all units", + given: NewUnitGroup( + &UnitStub{name: "unit1", typeName: "type", source: Source{}}, + ), + when: NewUnitGroup( + &UnitStub{name: "unit2", typeName: "type", source: Source{}}, + ), + then: NewUnitGroup( + &UnitStub{name: "unit1", typeName: "type", source: Source{}}, + &UnitStub{name: "unit2", typeName: "type", source: Source{}}, + ), + }, + { + name: "GIVEN two groups with overlapping units THEN return a group without duplicates", + given: NewUnitGroup( + &UnitStub{name: "unit1", typeName: "type", source: Source{}}, + ), + when: NewUnitGroup( + &UnitStub{name: "unit1", typeName: "type", source: Source{}}, + &UnitStub{name: "unit2", typeName: "type", source: Source{}}, + ), + then: NewUnitGroup( + &UnitStub{name: "unit1", typeName: "type", source: Source{}}, + &UnitStub{name: "unit2", typeName: "type", source: Source{}}, + ), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.given.Merge(tt.when) + require.Equal(t, tt.then, got) + }) + } +} + +func TestUnitGroup_Select(t *testing.T) { + tests := []struct { + name string + given UnitGroup + when func(u Unit) bool + then UnitGroup + }{ + { + name: "GIVEN no units matches, THEN return an empty group", + given: UnitGroup{ + &UnitStub{name: "unit2name", typeName: "type", source: Source{}}, + }, + when: func(u Unit) bool { + return false + }, + then: UnitGroup{}, + }, + { + name: "GIVEN units matches, THEN return a group with only matching units", + given: UnitGroup{ + &UnitStub{name: "unit1", typeName: "type", source: Source{}}, + &UnitStub{name: "unit2", typeName: "type", source: Source{}}, + }, + when: func(u Unit) bool { + return u.Name() == "unit2" + }, + then: UnitGroup{ + &UnitStub{name: "unit2", typeName: "type", source: Source{}}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.given.Select(tt.when) + require.Equal(t, tt.then, got) + }) + } +} + +func TestUnitGroup_SelectType(t *testing.T) { + tests := []struct { + name string + given UnitGroup + when UnitType + then UnitGroup + }{ + { + name: "GIVEN no units matches, THEN return an empty group", + given: UnitGroup{ + &UnitStub{name: "unit2name", typeName: "type", source: Source{}}, + }, + when: "not_found", + then: UnitGroup{}, + }, + { + name: "GIVEN a unit matches, THEN return a group with matching unit", + given: UnitGroup{ + &UnitStub{name: "unit1", typeName: "type1", source: Source{}}, + &UnitStub{name: "unit2", typeName: "type2", source: Source{}}, + }, + when: "type1", + then: UnitGroup{ + &UnitStub{name: "unit1", typeName: "type1", source: Source{}}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.given.SelectType(tt.when) + require.Equal(t, tt.then, got) + }) + } +} + +func TestUnitGroup_SelectName(t *testing.T) { + tests := []struct { + name string + given UnitGroup + when UnitName + then Unit + }{ + { + name: "GIVEN a group with multiple units WHEN selecting an existing name THEN return the corresponding unit", + given: NewUnitGroup( + &UnitStub{name: "unit1", typeName: "type", source: Source{}}, + &UnitStub{name: "unit2", typeName: "type", source: Source{}}, + ), + when: "unit2", + then: &UnitStub{name: "unit2", typeName: "type", source: Source{}}, + }, + { + name: "GIVEN a group with multiple units WHEN selecting a non-existent name THEN return nil", + given: NewUnitGroup( + &UnitStub{name: "unit1", typeName: "type", source: Source{}}, + &UnitStub{name: "unit2", typeName: "type", source: Source{}}, + ), + when: "spec3", + then: nil, + }, + { + name: "GIVEN an empty group WHEN selecting a name THEN return nil", + given: NewUnitGroup(), + when: "unit1", + then: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.given.SelectName(tt.when) + require.Equal(t, tt.then, got) + }) + } +} + +func TestUnitGroup_SelectNames(t *testing.T) { + tests := []struct { + name string + given UnitGroup + when []UnitName + then UnitGroup + }{ + { + name: "GIVEN no units matches, THEN return a group with no values", + given: UnitGroup{ + &UnitStub{name: "name", typeName: "type", source: Source{}}, + }, + when: []UnitName{"not_found"}, + then: UnitGroup{}, + }, + { + name: "GIVEN a unit matches, THEN return a group with matching unit", + given: UnitGroup{ + &UnitStub{name: "unit1", typeName: "type", source: Source{}}, + &UnitStub{name: "unit2", typeName: "type", source: Source{}}, + }, + when: []UnitName{"unit1"}, + then: UnitGroup{ + &UnitStub{name: "unit1", typeName: "type", source: Source{}}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.given.SelectNames(tt.when...) + require.Equal(t, tt.then, got) + }) + } +} + +func TestUnitGroup_Exclude(t *testing.T) { + tests := []struct { + name string + given UnitGroup + when func(u Unit) bool + then UnitGroup + }{ + { + name: "GIVEN no units matches, THEN return a group with the same values", + given: UnitGroup{ + &UnitStub{name: "name", typeName: "type", source: Source{}}, + }, + when: func(u Unit) bool { + return false + }, + then: UnitGroup{ + &UnitStub{name: "name", typeName: "type", source: Source{}}, + }, + }, + { + name: "GIVEN units matches, THEN return a group without matching units", + given: UnitGroup{ + &UnitStub{name: "unit1", typeName: "type", source: Source{}}, + &UnitStub{name: "unit2", typeName: "type", source: Source{}}, + }, + when: func(u Unit) bool { + return true + }, + then: UnitGroup{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.given.Exclude(tt.when) + require.Equal(t, tt.then, got) + }) + } +} + +func TestUnitGroup_ExcludeType(t *testing.T) { + tests := []struct { + name string + given UnitGroup + when UnitType + then UnitGroup + }{ + { + name: "GIVEN no units matches, THEN return a group with the same values", + given: UnitGroup{ + &UnitStub{name: "unit2name", typeName: "type", source: Source{}}, + }, + when: "not_found", + then: UnitGroup{ + &UnitStub{name: "unit2name", typeName: "type", source: Source{}}, + }, + }, + { + name: "GIVEN a unit matches, THEN return a group without matching unit", + given: UnitGroup{ + &UnitStub{name: "unit1", typeName: "type1", source: Source{}}, + &UnitStub{name: "unit2", typeName: "type2", source: Source{}}, + }, + when: "type1", + then: UnitGroup{ + &UnitStub{name: "unit2", typeName: "type2", source: Source{}}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.given.ExcludeType(tt.when) + require.Equal(t, tt.then, got) + }) + } +} + +func TestUnitGroup_ExcludeNames(t *testing.T) { + tests := []struct { + name string + given UnitGroup + when []UnitName + then UnitGroup + }{ + { + name: "GIVEN no units matches, THEN return a group with the same values", + given: UnitGroup{ + &UnitStub{name: "unit2name", typeName: "type", source: Source{}}, + }, + when: []UnitName{"not_found"}, + then: UnitGroup{ + &UnitStub{name: "unit2name", typeName: "type", source: Source{}}, + }, + }, + { + name: "GIVEN a unit matches, THEN return a group without matching unit", + given: UnitGroup{ + &UnitStub{name: "unit1", typeName: "type", source: Source{}}, + &UnitStub{name: "unit2", typeName: "type", source: Source{}}, + }, + when: []UnitName{"unit1"}, + then: UnitGroup{ + &UnitStub{name: "unit2", typeName: "type", source: Source{}}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.given.ExcludeNames(tt.when...) + require.Equal(t, tt.then, got) + }) + } +} + +func TestMapUnitGroup(t *testing.T) { + tests := []struct { + name string + given UnitGroup + when func(Unit) string + then []string + }{ + { + name: "GIVEN a group with multiple units WHEN mapped to their names THEN return a slice of unit names", + given: NewUnitGroup( + &UnitStub{name: "unit1", typeName: "type", source: Source{}}, + &UnitStub{name: "unit2", typeName: "type", source: Source{}}, + ), + when: func(u Unit) string { + return string(u.Name()) + }, + then: []string{"unit1", "unit2"}, + }, + { + name: "GIVEN an empty group WHEN mapped THEN return a nil slice", + given: NewUnitGroup(), + when: func(u Unit) string { + return string(u.Name()) + }, + then: nil, + }, + { + name: "GIVEN a group with multiple units WHEN mapped to a constant value THEN return a slice of that value", + given: NewUnitGroup( + &UnitStub{name: "unit1", typeName: "type", source: Source{}}, + &UnitStub{name: "unit2", typeName: "type", source: Source{}}, + ), + when: func(u Unit) string { + return "constant" + }, + then: []string{"constant", "constant"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := MapUnitGroup(tt.given, tt.when) + assert.Equal(t, tt.then, got) + }) + } +} diff --git a/pkg/specter/specproc.go b/pkg/specter/unitproc.go similarity index 79% rename from pkg/specter/specproc.go rename to pkg/specter/unitproc.go index 8853ea1..2aa6ff9 100644 --- a/pkg/specter/specproc.go +++ b/pkg/specter/unitproc.go @@ -18,9 +18,9 @@ import "context" type ProcessingContext struct { context.Context - Specifications SpecificationGroup - Artifacts []Artifact - Logger Logger + Units UnitGroup + Artifacts []Artifact + Logger Logger } // Artifact returns an artifact by its ID. @@ -33,20 +33,20 @@ func (c ProcessingContext) Artifact(id ArtifactID) Artifact { return nil } -// SpecificationProcessor are services responsible for performing work using Specifications +// UnitProcessor are services responsible for performing work using Units // and which can possibly generate artifacts. -type SpecificationProcessor interface { +type UnitProcessor interface { // Name returns the unique name of this processor. // This name can appear in logs to report information about a given processor. Name() string - // Process processes a group of specifications. + // Process processes a group of units. Process(ctx ProcessingContext) ([]Artifact, error) } -// ArtifactProcessor are services responsible for processing artifacts of SpecProcessors. +// ArtifactProcessor are services responsible for processing artifacts of UnitProcessors. type ArtifactProcessor interface { - // Process performs the processing of artifacts generated by SpecificationProcessor. + // Process performs the processing of artifacts generated by UnitProcessor. Process(ctx ArtifactProcessingContext) error // Name returns the name of this processor. diff --git a/pkg/specter/specproc_test.go b/pkg/specter/unitproc_test.go similarity index 100% rename from pkg/specter/specproc_test.go rename to pkg/specter/unitproc_test.go diff --git a/pkg/specterutils/README.md b/pkg/specterutils/README.md index aba9c3c..2950b3d 100644 --- a/pkg/specterutils/README.md +++ b/pkg/specterutils/README.md @@ -1,12 +1,12 @@ -# SpecterUtils +# UnitterUtils -SpecterUtils is a collection of utilities and extensions for the Specter library, designed to enhance and extend its +UnitterUtils is a collection of utilities and extensions for the Unitter library, designed to enhance and extend its functionality. This package includes various "batteries included" features, such as linting and dependency resolution, -that complement the core features of Specter. +that complement the core features of Unitter. Overview -SpecterUtils provides additional tools and utilities that integrate with Specter. -It aims to enhance the capabilities of Specter without being part of the core package. -These utilities help with tasks such as linting your specifications and resolving dependencies, +UnitterUtils provides additional tools and utilities that integrate with Unitter. +It aims to enhance the capabilities of Unitter without being part of the core package. +These utilities help with tasks such as linting your units and resolving dependencies, making your development process smoother and more efficient. diff --git a/pkg/specterutils/depresolve.go b/pkg/specterutils/depresolve.go index 2ebc8c6..c00731d 100644 --- a/pkg/specterutils/depresolve.go +++ b/pkg/specterutils/depresolve.go @@ -23,20 +23,20 @@ import ( const ResolvedDependenciesArtifactID = "_resolved_dependencies" -// ResolvedDependencies represents an ordered list of Specification that should be processed in that specific order to avoid +// ResolvedDependencies represents an ordered list of Unit that should be processed in that specific order to avoid // unresolved types. -type ResolvedDependencies specter.SpecificationGroup +type ResolvedDependencies specter.UnitGroup func (r ResolvedDependencies) ID() specter.ArtifactID { return ResolvedDependenciesArtifactID } type DependencyProvider interface { - Supports(s specter.Specification) bool - Provide(s specter.Specification) []specter.SpecificationName + Supports(s specter.Unit) bool + Provide(s specter.Unit) []specter.UnitName } -var _ specter.SpecificationProcessor = DependencyResolutionProcessor{} +var _ specter.UnitProcessor = DependencyResolutionProcessor{} type DependencyResolutionProcessor struct { providers []DependencyProvider @@ -54,8 +54,8 @@ func (p DependencyResolutionProcessor) Process(ctx specter.ProcessingContext) ([ ctx.Logger.Info("\nResolving dependencies...") var nodes []dependencyNode - for _, s := range ctx.Specifications { - node := dependencyNode{Specification: s, Dependencies: nil} + for _, s := range ctx.Units { + node := dependencyNode{Unit: s, Dependencies: nil} for _, provider := range p.providers { if !provider.Supports(s) { continue @@ -80,9 +80,9 @@ func GetResolvedDependenciesFromContext(ctx specter.ProcessingContext) ResolvedD return specter.GetContextArtifact[ResolvedDependencies](ctx, ResolvedDependenciesArtifactID) } -type dependencySet map[specter.SpecificationName]struct{} +type dependencySet map[specter.UnitName]struct{} -func newDependencySet(dependencies ...specter.SpecificationName) dependencySet { +func newDependencySet(dependencies ...specter.UnitName) dependencySet { deps := dependencySet{} for _, d := range dependencies { deps[d] = struct{}{} @@ -105,31 +105,31 @@ func (s dependencySet) diff(o dependencySet) dependencySet { } type dependencyNode struct { - Specification specter.Specification - Dependencies dependencySet + Unit specter.Unit + Dependencies dependencySet } -func (d dependencyNode) SpecificationName() specter.SpecificationName { - return d.Specification.Name() +func (d dependencyNode) UnitName() specter.UnitName { + return d.Unit.Name() } type dependencyGraph []dependencyNode -func newDependencyGraph(specifications ...dependencyNode) dependencyGraph { - return append(dependencyGraph{}, specifications...) +func newDependencyGraph(units ...dependencyNode) dependencyGraph { + return append(dependencyGraph{}, units...) } func (g dependencyGraph) resolve() (ResolvedDependencies, error) { var resolved ResolvedDependencies // Look up of nodes to their typeName Names. - specByTypeNames := map[specter.SpecificationName]specter.Specification{} + specByTypeNames := map[specter.UnitName]specter.Unit{} // Map nodes to dependencies - dependenciesByTypeNames := map[specter.SpecificationName]dependencySet{} + dependenciesByTypeNames := map[specter.UnitName]dependencySet{} for _, n := range g { - specByTypeNames[n.SpecificationName()] = n.Specification - dependenciesByTypeNames[n.SpecificationName()] = n.Dependencies + specByTypeNames[n.UnitName()] = n.Unit + dependenciesByTypeNames[n.UnitName()] = n.Dependencies } // The algorithm simply processes all nodes and tries to find the ones that have no dependencies. @@ -137,7 +137,7 @@ func (g dependencyGraph) resolve() (ResolvedDependencies, error) { // If no unresolvable or circular dependency is found, the node is considered resolved. // And processing retries with the remaining dependent nodes. for len(dependenciesByTypeNames) != 0 { - var typeNamesWithNoDependencies []specter.SpecificationName + var typeNamesWithNoDependencies []specter.UnitName for typeName, dependencies := range dependenciesByTypeNames { if len(dependencies) == 0 { typeNamesWithNoDependencies = append(typeNamesWithNoDependencies, typeName) @@ -155,7 +155,7 @@ func (g dependencyGraph) resolve() (ResolvedDependencies, error) { if _, found := specByTypeNames[dependency]; !found { return nil, errors.NewWithMessage( errors.InternalErrorCode, - fmt.Sprintf("specification with type %q depends on an unresolved type %q", + fmt.Sprintf("unit with type %q depends on an unresolved type %q", typeName, dependency, ), @@ -195,24 +195,24 @@ func (g dependencyGraph) resolve() (ResolvedDependencies, error) { return resolved, nil } -// HasDependencies is an interface that can be implemented by specifications +// HasDependencies is an interface that can be implemented by units // that define their dependencies from their field values. // This interface can be used in conjunction with the HasDependenciesProvider // to easily resolve dependencies. type HasDependencies interface { - specter.Specification - Dependencies() []specter.SpecificationName + specter.Unit + Dependencies() []specter.UnitName } type HasDependenciesProvider struct{} -func (h HasDependenciesProvider) Supports(s specter.Specification) bool { - _, ok := s.(HasDependencies) +func (h HasDependenciesProvider) Supports(u specter.Unit) bool { + _, ok := u.(HasDependencies) return ok } -func (h HasDependenciesProvider) Provide(s specter.Specification) []specter.SpecificationName { - d, ok := s.(HasDependencies) +func (h HasDependenciesProvider) Provide(u specter.Unit) []specter.UnitName { + d, ok := u.(HasDependencies) if !ok { return nil } diff --git a/pkg/specterutils/depresolve_test.go b/pkg/specterutils/depresolve_test.go index 7a90b71..6a33207 100644 --- a/pkg/specterutils/depresolve_test.go +++ b/pkg/specterutils/depresolve_test.go @@ -26,25 +26,25 @@ import ( // MockDependencyProvider is a mock implementation of DependencyProvider for testing. type MockDependencyProvider struct { - supportFunc func(s specter.Specification) bool - provideFunc func(s specter.Specification) []specter.SpecificationName + supportFunc func(specter.Unit) bool + provideFunc func(specter.Unit) []specter.UnitName } -func (m *MockDependencyProvider) Supports(s specter.Specification) bool { - return m.supportFunc(s) +func (m *MockDependencyProvider) Supports(u specter.Unit) bool { + return m.supportFunc(u) } -func (m *MockDependencyProvider) Provide(s specter.Specification) []specter.SpecificationName { - return m.provideFunc(s) +func (m *MockDependencyProvider) Provide(u specter.Unit) []specter.UnitName { + return m.provideFunc(u) } func TestDependencyResolutionProcessor_Process(t *testing.T) { type args struct { - specifications []specter.Specification - providers []DependencyProvider + units []specter.Unit + providers []DependencyProvider } - spec1 := NewGenericSpecification("spec1", "type", specter.Source{}) - spec2 := NewGenericSpecification("spec2", "type", specter.Source{}) + unit1 := NewGenericUnit("unit1", "type", specter.Source{}) + unit2 := NewGenericUnit("unit2", "type", specter.Source{}) tests := []struct { name string given args @@ -54,8 +54,8 @@ func TestDependencyResolutionProcessor_Process(t *testing.T) { { name: "GIVEN no providers THEN returns nil", given: args{ - providers: nil, - specifications: nil, + providers: nil, + units: nil, }, then: nil, expectedError: nil, @@ -65,33 +65,33 @@ func TestDependencyResolutionProcessor_Process(t *testing.T) { given: args{ providers: []DependencyProvider{ &MockDependencyProvider{ - supportFunc: func(s specter.Specification) bool { + supportFunc: func(_ specter.Unit) bool { return false }, - provideFunc: func(s specter.Specification) []specter.SpecificationName { + provideFunc: func(_ specter.Unit) []specter.UnitName { return nil }, }, &MockDependencyProvider{ - supportFunc: func(s specter.Specification) bool { - return s.Type() == "type" + supportFunc: func(u specter.Unit) bool { + return u.Type() == "type" }, - provideFunc: func(s specter.Specification) []specter.SpecificationName { - if s.Name() == "spec1" { - return []specter.SpecificationName{"spec2"} + provideFunc: func(u specter.Unit) []specter.UnitName { + if u.Name() == "unit1" { + return []specter.UnitName{"unit2"} } return nil }, }, }, - specifications: specter.SpecificationGroup{ - spec1, - spec2, + units: specter.UnitGroup{ + unit1, + unit2, }, }, then: ResolvedDependencies{ - spec2, // topological sort - spec1, + unit2, // topological sort + unit1, }, expectedError: nil, }, @@ -100,22 +100,22 @@ func TestDependencyResolutionProcessor_Process(t *testing.T) { given: args{ providers: []DependencyProvider{ &MockDependencyProvider{ - supportFunc: func(s specter.Specification) bool { - return s.Type() == "type" + supportFunc: func(u specter.Unit) bool { + return u.Type() == "type" }, - provideFunc: func(s specter.Specification) []specter.SpecificationName { - if s.Name() == "spec1" { - return []specter.SpecificationName{"spec2"} - } else if s.Name() == "spec2" { - return []specter.SpecificationName{"spec1"} + provideFunc: func(u specter.Unit) []specter.UnitName { + if u.Name() == "unit1" { + return []specter.UnitName{"unit2"} + } else if u.Name() == "unit2" { + return []specter.UnitName{"unit1"} } return nil }, }, }, - specifications: specter.SpecificationGroup{ - spec1, - spec2, + units: specter.UnitGroup{ + unit1, + unit2, }, }, then: nil, @@ -126,17 +126,17 @@ func TestDependencyResolutionProcessor_Process(t *testing.T) { given: args{ providers: []DependencyProvider{ &MockDependencyProvider{ - supportFunc: func(s specter.Specification) bool { - return s.Type() == "type" + supportFunc: func(u specter.Unit) bool { + return u.Type() == "type" }, - provideFunc: func(s specter.Specification) []specter.SpecificationName { - return []specter.SpecificationName{"spec3"} + provideFunc: func(u specter.Unit) []specter.UnitName { + return []specter.UnitName{"spec3"} }, }, }, - specifications: specter.SpecificationGroup{ - spec1, - spec2, // spec2 is not provided + units: specter.UnitGroup{ + unit1, + unit2, // unit2 is not provided }, }, then: nil, @@ -148,7 +148,7 @@ func TestDependencyResolutionProcessor_Process(t *testing.T) { processor := NewDependencyResolutionProcessor(tt.given.providers...) ctx := specter.ProcessingContext{ - Specifications: tt.given.specifications, + Units: tt.given.units, Logger: specter.NewDefaultLogger(specter.DefaultLoggerConfig{ DisableColors: true, Writer: os.Stdout, @@ -183,12 +183,12 @@ func TestGetResolvedDependenciesFromContext(t *testing.T) { given: specter.ProcessingContext{ Artifacts: []specter.Artifact{ ResolvedDependencies{ - NewGenericSpecification("name", "type", specter.Source{}), + NewGenericUnit("name", "type", specter.Source{}), }, }, }, want: ResolvedDependencies{ - NewGenericSpecification("name", "type", specter.Source{}), + NewGenericUnit("name", "type", specter.Source{}), }, }, { @@ -219,49 +219,49 @@ func TestDependencyResolutionProcessor_Name(t *testing.T) { assert.NotEqual(t, "", p.Name()) } -type hasDependencySpec struct { +type hasDependencyUnit struct { source specter.Source - dependencies []specter.SpecificationName + dependencies []specter.UnitName } -func (h *hasDependencySpec) Name() specter.SpecificationName { - return "spec" +func (h *hasDependencyUnit) Name() specter.UnitName { + return "unit" } -func (h *hasDependencySpec) Type() specter.SpecificationType { - return "spec" +func (h *hasDependencyUnit) Type() specter.UnitType { + return "unit" } -func (h *hasDependencySpec) Description() string { +func (h *hasDependencyUnit) Description() string { return "description" } -func (h *hasDependencySpec) Source() specter.Source { +func (h *hasDependencyUnit) Source() specter.Source { return h.source } -func (h *hasDependencySpec) SetSource(s specter.Source) { +func (h *hasDependencyUnit) SetSource(s specter.Source) { h.source = s } -func (h *hasDependencySpec) Dependencies() []specter.SpecificationName { +func (h *hasDependencyUnit) Dependencies() []specter.UnitName { return h.dependencies } func TestHasDependenciesProvider_Supports(t *testing.T) { tests := []struct { name string - given specter.Specification + given specter.Unit then bool }{ { - name: "GIVEN specification not implementing HasDependencies THEN return false", - given: &GenericSpecification{}, + name: "GIVEN unit not implementing HasDependencies THEN return false", + given: &GenericUnit{}, then: false, }, { - name: "GIVEN specification implementing HasDependencies THEN return false", - given: &hasDependencySpec{}, + name: "GIVEN unit implementing HasDependencies THEN return false", + given: &hasDependencyUnit{}, then: true, }, } @@ -276,18 +276,18 @@ func TestHasDependenciesProvider_Supports(t *testing.T) { func TestHasDependenciesProvider_Provide(t *testing.T) { tests := []struct { name string - given specter.Specification - then []specter.SpecificationName + given specter.Unit + then []specter.UnitName }{ { - name: "GIVEN specification not implementing HasDependencies THEN return nil", - given: &GenericSpecification{}, + name: "GIVEN unit not implementing HasDependencies THEN return nil", + given: &GenericUnit{}, then: nil, }, { - name: "GIVEN specification implementing HasDependencies THEN return dependencies", - given: &hasDependencySpec{dependencies: []specter.SpecificationName{"spec1"}}, - then: []specter.SpecificationName{"spec1"}, + name: "GIVEN unit implementing HasDependencies THEN return dependencies", + given: &hasDependencyUnit{dependencies: []specter.UnitName{"unit1"}}, + then: []specter.UnitName{"unit1"}, }, } for _, tt := range tests { diff --git a/pkg/specterutils/genericspec.go b/pkg/specterutils/genericunit.go similarity index 59% rename from pkg/specterutils/genericspec.go rename to pkg/specterutils/genericunit.go index 652d0d5..c0843d8 100644 --- a/pkg/specterutils/genericspec.go +++ b/pkg/specterutils/genericunit.go @@ -20,47 +20,47 @@ import ( "github.com/zclconf/go-cty/cty" ) -// GenericSpecification is a generic implementation of a Specification that saves its attributes in a list of attributes for introspection. +// GenericUnit is a generic implementation of a Unit that saves its attributes in a list of attributes for introspection. // these can be useful for loaders that are looser in what they allow. -type GenericSpecification struct { - name specter.SpecificationName - typ specter.SpecificationType +type GenericUnit struct { + name specter.UnitName + typ specter.UnitType source specter.Source - Attributes []GenericSpecAttribute + Attributes []GenericUnitAttribute } -func NewGenericSpecification(name specter.SpecificationName, typ specter.SpecificationType, source specter.Source) *GenericSpecification { - return &GenericSpecification{name: name, typ: typ, source: source} +func NewGenericUnit(name specter.UnitName, typ specter.UnitType, source specter.Source) *GenericUnit { + return &GenericUnit{name: name, typ: typ, source: source} } -func (s *GenericSpecification) SetSource(src specter.Source) { - s.source = src +func (u *GenericUnit) SetSource(src specter.Source) { + u.source = src } -func (s *GenericSpecification) Description() string { - if !s.HasAttribute("description") { +func (u *GenericUnit) Description() string { + if !u.HasAttribute("description") { return "" } - attr := s.Attribute("description") + attr := u.Attribute("description") return attr.Value.String() } -func (s *GenericSpecification) Name() specter.SpecificationName { - return s.name +func (u *GenericUnit) Name() specter.UnitName { + return u.name } -func (s *GenericSpecification) Type() specter.SpecificationType { - return s.typ +func (u *GenericUnit) Type() specter.UnitType { + return u.typ } -func (s *GenericSpecification) Source() specter.Source { - return s.source +func (u *GenericUnit) Source() specter.Source { + return u.source } // Attribute returns an attribute by its FilePath or nil if it was not found. -func (s *GenericSpecification) Attribute(name string) *GenericSpecAttribute { - for _, a := range s.Attributes { +func (u *GenericUnit) Attribute(name string) *GenericUnitAttribute { + for _, a := range u.Attributes { if a.Name == name { return &a } @@ -69,9 +69,9 @@ func (s *GenericSpecification) Attribute(name string) *GenericSpecAttribute { return nil } -// HasAttribute indicates if a specification has a certain attribute or not. -func (s *GenericSpecification) HasAttribute(name string) bool { - for _, a := range s.Attributes { +// HasAttribute indicates if a unit has a certain attribute or not. +func (u *GenericUnit) HasAttribute(name string) bool { + for _, a := range u.Attributes { if a.Name == name { return true } @@ -87,9 +87,9 @@ const ( Unknown = "any" ) -// GenericSpecAttribute represents an attribute of a specification. +// GenericUnitAttribute represents an attribute of a unit. // It relies on cty.Value to represent the loaded value. -type GenericSpecAttribute struct { +type GenericUnitAttribute struct { Name string Value AttributeValue } @@ -119,7 +119,7 @@ var _ AttributeValue = ObjectValue{} // ObjectValue represents a type of attribute value that is a nested data structure as opposed to a scalar value. type ObjectValue struct { Type AttributeType - Attributes []GenericSpecAttribute + Attributes []GenericUnitAttribute } func (o ObjectValue) String() string { diff --git a/pkg/specterutils/genericspec_test.go b/pkg/specterutils/genericunit_test.go similarity index 57% rename from pkg/specterutils/genericspec_test.go rename to pkg/specterutils/genericunit_test.go index fd7dbfe..17f6b01 100644 --- a/pkg/specterutils/genericspec_test.go +++ b/pkg/specterutils/genericunit_test.go @@ -22,35 +22,35 @@ import ( "testing" ) -func TestGenericSpecification_Description(t *testing.T) { +func TestGenericUnit_Description(t *testing.T) { tests := []struct { name string - given *specterutils.GenericSpecification + given *specterutils.GenericUnit then string }{ { - name: "GIVEN a specification with a description attribute THEN return the description", - given: &specterutils.GenericSpecification{ - Attributes: []specterutils.GenericSpecAttribute{ + name: "GIVEN a unit with a description attribute THEN return the description", + given: &specterutils.GenericUnit{ + Attributes: []specterutils.GenericUnitAttribute{ { Name: "description", - Value: specterutils.GenericValue{Value: cty.StringVal("This is a test specification")}, + Value: specterutils.GenericValue{Value: cty.StringVal("This is a test unit")}, }, }, }, - then: "This is a test specification", + then: "This is a test unit", }, { - name: "GIVEN a specification without a description attribute THEN return an empty string", - given: &specterutils.GenericSpecification{ - Attributes: []specterutils.GenericSpecAttribute{}, + name: "GIVEN a unit without a description attribute THEN return an empty string", + given: &specterutils.GenericUnit{ + Attributes: []specterutils.GenericUnitAttribute{}, }, then: "", }, { - name: "GIVEN a specification with a non-string description THEN return an empty string", - given: &specterutils.GenericSpecification{ - Attributes: []specterutils.GenericSpecAttribute{ + name: "GIVEN a unit with a non-string description THEN return an empty string", + given: &specterutils.GenericUnit{ + Attributes: []specterutils.GenericUnitAttribute{ { Name: "description", Value: specterutils.GenericValue{Value: cty.NumberIntVal(42)}, // Not a string value @@ -69,17 +69,17 @@ func TestGenericSpecification_Description(t *testing.T) { } } -func TestGenericSpecification_Attribute(t *testing.T) { +func TestGenericUnit_Attribute(t *testing.T) { tests := []struct { name string - given *specterutils.GenericSpecification + given *specterutils.GenericUnit when string - then *specterutils.GenericSpecAttribute + then *specterutils.GenericUnitAttribute }{ { - name: "GIVEN a specification with a specific attribute WHEN Attribute is called THEN return the attribute", - given: &specterutils.GenericSpecification{ - Attributes: []specterutils.GenericSpecAttribute{ + name: "GIVEN a unit with a specific attribute WHEN Attribute is called THEN return the attribute", + given: &specterutils.GenericUnit{ + Attributes: []specterutils.GenericUnitAttribute{ { Name: "attr1", Value: specterutils.GenericValue{Value: cty.StringVal("value1")}, @@ -87,15 +87,15 @@ func TestGenericSpecification_Attribute(t *testing.T) { }, }, when: "attr1", - then: &specterutils.GenericSpecAttribute{ + then: &specterutils.GenericUnitAttribute{ Name: "attr1", Value: specterutils.GenericValue{Value: cty.StringVal("value1")}, }, }, { - name: "GIVEN a specification without the specified attribute WHEN Attribute is called THEN return nil", - given: &specterutils.GenericSpecification{ - Attributes: []specterutils.GenericSpecAttribute{}, + name: "GIVEN a unit without the specified attribute WHEN Attribute is called THEN return nil", + given: &specterutils.GenericUnit{ + Attributes: []specterutils.GenericUnitAttribute{}, }, when: "nonexistent", then: nil, @@ -110,17 +110,17 @@ func TestGenericSpecification_Attribute(t *testing.T) { } } -func TestGenericSpecification_HasAttribute(t *testing.T) { +func TestGenericUnit_HasAttribute(t *testing.T) { tests := []struct { name string - given *specterutils.GenericSpecification + given *specterutils.GenericUnit when string then bool }{ { - name: "GIVEN a specification with a specific attribute WHEN HasAttribute is called THEN return true", - given: &specterutils.GenericSpecification{ - Attributes: []specterutils.GenericSpecAttribute{ + name: "GIVEN a unit with a specific attribute WHEN HasAttribute is called THEN return true", + given: &specterutils.GenericUnit{ + Attributes: []specterutils.GenericUnitAttribute{ { Name: "attr1", Value: specterutils.GenericValue{Value: cty.StringVal("value1")}, @@ -131,9 +131,9 @@ func TestGenericSpecification_HasAttribute(t *testing.T) { then: true, }, { - name: "GIVEN a specification without the specified attribute WHEN HasAttribute is called THEN return false", - given: &specterutils.GenericSpecification{ - Attributes: []specterutils.GenericSpecAttribute{}, + name: "GIVEN a unit without the specified attribute WHEN HasAttribute is called THEN return false", + given: &specterutils.GenericUnit{ + Attributes: []specterutils.GenericUnitAttribute{}, }, when: "nonexistent", then: false, @@ -148,16 +148,16 @@ func TestGenericSpecification_HasAttribute(t *testing.T) { } } -func TestGenericSpecification_SetSource(t *testing.T) { +func TestGenericUnit_SetSource(t *testing.T) { tests := []struct { name string - given *specterutils.GenericSpecification + given *specterutils.GenericUnit when specter.Source then specter.Source }{ { - name: "GIVEN a specification WHEN SetSource is called THEN updates the source", - given: specterutils.NewGenericSpecification("name", "type", specter.Source{Location: "initial/path"}), + name: "GIVEN a unit WHEN SetSource is called THEN updates the source", + given: specterutils.NewGenericUnit("name", "type", specter.Source{Location: "initial/path"}), when: specter.Source{Location: "new/path"}, then: specter.Source{Location: "new/path"}, }, @@ -172,7 +172,7 @@ func TestGenericSpecification_SetSource(t *testing.T) { } func TestObjectValue_String(t *testing.T) { - o := specterutils.ObjectValue{Type: "hello", Attributes: []specterutils.GenericSpecAttribute{ + o := specterutils.ObjectValue{Type: "hello", Attributes: []specterutils.GenericUnitAttribute{ {Name: "hello", Value: specterutils.GenericValue{Value: cty.StringVal("world")}}, }} assert.Equal(t, "ObjectValue{Type: hello, Attributes: [{hello world}]}", o.String()) diff --git a/pkg/specterutils/hcl.go b/pkg/specterutils/hcl.go index 0646552..d6e1783 100644 --- a/pkg/specterutils/hcl.go +++ b/pkg/specterutils/hcl.go @@ -31,33 +31,33 @@ const ( const InvalidHCLErrorCode = "specter.spec_loading.invalid_hcl" -// NewHCLGenericSpecLoader this SpecificationLoader will load all Specifications to instances of GenericSpecification. -func NewHCLGenericSpecLoader() *HCLGenericSpecLoader { - return &HCLGenericSpecLoader{ +// NewHCLGenericUnitLoader this UnitLoader will load all Units to instances of GenericUnit. +func NewHCLGenericUnitLoader() *HCLGenericUnitLoader { + return &HCLGenericUnitLoader{ Parser: *hclparse.NewParser(), } } -// HCLGenericSpecLoader this SpecificationLoader loads Specifications as GenericSpecification. -type HCLGenericSpecLoader struct { +// HCLGenericUnitLoader this UnitLoader loads Units as GenericUnit. +type HCLGenericUnitLoader struct { hclparse.Parser } -func (l HCLGenericSpecLoader) SupportsSource(s specter.Source) bool { +func (l HCLGenericUnitLoader) SupportsSource(s specter.Source) bool { return s.Format == HCLSourceFormat } -func (l HCLGenericSpecLoader) Load(s specter.Source) ([]specter.Specification, error) { +func (l HCLGenericUnitLoader) Load(s specter.Source) ([]specter.Unit, error) { ctx := &hcl.EvalContext{ Variables: map[string]cty.Value{}, } - // Although the caller is responsible for calling HCLGenericSpecLoader.SupportsSource, guard against it. + // Although the caller is responsible for calling HCLGenericUnitLoader.SupportsSource, guard against it. if !l.SupportsSource(s) { return nil, errors.NewWithMessage( specter.UnsupportedSourceErrorCode, fmt.Sprintf( - "invalid specification source %q, unsupported format %q", + "invalid unit source %q, unsupported format %q", s.Location, s.Format, ), @@ -69,7 +69,7 @@ func (l HCLGenericSpecLoader) Load(s specter.Source) ([]specter.Specification, e return nil, errors.Wrap(diags, InvalidHCLErrorCode) } - var specifications []specter.Specification + var units []specter.Unit body := file.Body.(*hclsyntax.Body) for _, block := range body.Blocks { @@ -78,7 +78,7 @@ func (l HCLGenericSpecLoader) Load(s specter.Source) ([]specter.Specification, e return nil, errors.NewWithMessage( InvalidHCLErrorCode, fmt.Sprintf( - "invalid specification source %q at line %d:%d, block %q should contain a name", + "invalid unit source %q at line %d:%d, block %q should contain a name", s.Location, block.Range().Start.Line, block.Range().Start.Column, @@ -96,8 +96,8 @@ func (l HCLGenericSpecLoader) Load(s specter.Source) ([]specter.Specification, e // err, // InvalidHCLErrorCode, // fmt.Sprintf( - // "invalid specification source %q at line %d:%d for block %q", - // s.Location, + // "invalid unit source %q at line %d:%d for block %q", + // u.Location, // block.Range().Start.Line, // block.Range().Start.Column, // block.Type, @@ -105,10 +105,10 @@ func (l HCLGenericSpecLoader) Load(s specter.Source) ([]specter.Specification, e //) } - // Create specification and add to list - specifications = append(specifications, &GenericSpecification{ - name: specter.SpecificationName(block.Labels[0]), - typ: specter.SpecificationType(block.Type), + // Create unit and add to list + units = append(units, &GenericUnit{ + name: specter.UnitName(block.Labels[0]), + typ: specter.UnitType(block.Type), source: s, Attributes: specAttributes, }) @@ -121,11 +121,11 @@ func (l HCLGenericSpecLoader) Load(s specter.Source) ([]specter.Specification, e } } - return specifications, errors.GroupOrNil(group) + return units, errors.GroupOrNil(group) } -func (l HCLGenericSpecLoader) extractAttributesFromBlock(ctx *hcl.EvalContext, block *hclsyntax.Block) ([]GenericSpecAttribute, hcl.Diagnostics) { - var attrs []GenericSpecAttribute +func (l HCLGenericUnitLoader) extractAttributesFromBlock(ctx *hcl.EvalContext, block *hclsyntax.Block) ([]GenericUnitAttribute, hcl.Diagnostics) { + var attrs []GenericUnitAttribute var diags hcl.Diagnostics @@ -137,7 +137,7 @@ func (l HCLGenericSpecLoader) extractAttributesFromBlock(ctx *hcl.EvalContext, b continue } - attrs = append(attrs, GenericSpecAttribute{ + attrs = append(attrs, GenericUnitAttribute{ Name: a.Name, Value: GenericValue{value}, }) @@ -156,7 +156,7 @@ func (l HCLGenericSpecLoader) extractAttributesFromBlock(ctx *hcl.EvalContext, b continue } - attrs = append(attrs, GenericSpecAttribute{ + attrs = append(attrs, GenericUnitAttribute{ Name: bName, Value: ObjectValue{ Type: AttributeType(b.Type), @@ -168,11 +168,11 @@ func (l HCLGenericSpecLoader) extractAttributesFromBlock(ctx *hcl.EvalContext, b return attrs, diags } -type HCLSpecLoaderFileConfigurationProvider func() HCLFileConfig +type HCLUnitLoaderFileConfigurationProvider func() HCLFileConfig -// HCLFileConfig interface that is to be implemented to define the structure of HCL specification files. +// HCLFileConfig interface that is to be implemented to define the structure of HCL unit files. type HCLFileConfig interface { - Specifications() []specter.Specification + Units() []specter.Unit } // HCLVariableConfig represents a block configuration that allows defining variables. @@ -182,16 +182,16 @@ type HCLVariableConfig struct { Value cty.Value `hcl:"value"` } -// HCLSpecLoader this loader allows to load Specifications to typed structs by providing a HCLFileConfig. -type HCLSpecLoader struct { +// HCLUnitLoader this loader allows to load Units to typed structs by providing a HCLFileConfig. +type HCLUnitLoader struct { // represents the structure of a file that this HCL loader should support. parser *hclparse.Parser - fileConfigProvider HCLSpecLoaderFileConfigurationProvider + fileConfigProvider HCLUnitLoaderFileConfigurationProvider evalCtx *hcl.EvalContext } -func NewHCLSpecLoader(fileConfigProvider HCLSpecLoaderFileConfigurationProvider) *HCLSpecLoader { - return &HCLSpecLoader{ +func NewHCLUnitLoader(fileConfigProvider HCLUnitLoaderFileConfigurationProvider) *HCLUnitLoader { + return &HCLUnitLoader{ fileConfigProvider: fileConfigProvider, evalCtx: &hcl.EvalContext{ Variables: map[string]cty.Value{}, @@ -200,13 +200,13 @@ func NewHCLSpecLoader(fileConfigProvider HCLSpecLoaderFileConfigurationProvider) } } -func (l HCLSpecLoader) Load(s specter.Source) ([]specter.Specification, error) { - // Although the caller is responsible for calling HCLGenericSpecLoader.SupportsSource, guard against it. +func (l HCLUnitLoader) Load(s specter.Source) ([]specter.Unit, error) { + // Although the caller is responsible for calling HCLGenericUnitLoader.SupportsSource, guard against it. if !l.SupportsSource(s) { return nil, errors.NewWithMessage( specter.UnsupportedSourceErrorCode, fmt.Sprintf( - "invalid specification source %q, unsupported format %q", + "invalid unit source %q, unsupported format %q", s.Location, s.Format, ), @@ -217,7 +217,7 @@ func (l HCLSpecLoader) Load(s specter.Source) ([]specter.Specification, error) { //// Parse const blocks to add them as Variables in the context. //var diags hcl.Diagnostics //var parsedFile *hcl.File - //parsedFile, diags = l.parser.ParseHCL(s.Data, s.Location) + //parsedFile, diags = l.parser.ParseHCL(u.Data, u.Location) // //body := parsedFile.Body.(*hclsyntax.Body) //for _, b := range body.Blocks { @@ -243,14 +243,14 @@ func (l HCLSpecLoader) Load(s specter.Source) ([]specter.Specification, error) { return nil, errors.Wrap(err, InvalidHCLErrorCode) } - // Set source for all specifications - specifications := fileConf.Specifications() - for _, sp := range specifications { + // Set source for all units + units := fileConf.Units() + for _, sp := range units { sp.SetSource(s) } - return specifications, nil + return units, nil } -func (l HCLSpecLoader) SupportsSource(s specter.Source) bool { +func (l HCLUnitLoader) SupportsSource(s specter.Source) bool { return s.Format == HCLSourceFormat } diff --git a/pkg/specterutils/hcl_test.go b/pkg/specterutils/hcl_test.go index 7c73f88..87a7401 100644 --- a/pkg/specterutils/hcl_test.go +++ b/pkg/specterutils/hcl_test.go @@ -22,7 +22,7 @@ import ( "testing" ) -func TestHCLGenericSpecLoader_SupportsSource(t *testing.T) { +func TestHCLGenericUnitLoader_SupportsSource(t *testing.T) { type when struct { source specter.Source } @@ -51,19 +51,19 @@ func TestHCLGenericSpecLoader_SupportsSource(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := NewHCLGenericSpecLoader() + l := NewHCLGenericUnitLoader() assert.Equalf(t, tt.then.supports, l.SupportsSource(tt.when.source), "SupportsSource(%v)", tt.when.source) }) } } -func TestHCLGenericSpecLoader_Load(t *testing.T) { +func TestHCLGenericUnitLoader_Load(t *testing.T) { type when struct { source specter.Source } type then struct { - expectedSpecifications []specter.Specification - expectedError require.ErrorAssertionFunc + expectedUnits []specter.Unit + expectedError require.ErrorAssertionFunc } mockFile := HclConfigMock{} @@ -82,8 +82,8 @@ func TestHCLGenericSpecLoader_Load(t *testing.T) { }, }, then: then{ - expectedSpecifications: nil, - expectedError: require.NoError, + expectedUnits: nil, + expectedError: require.NoError, }, }, { @@ -92,8 +92,8 @@ func TestHCLGenericSpecLoader_Load(t *testing.T) { source: mockFile.source(), }, then: then{ - expectedSpecifications: []specter.Specification{ - mockFile.genericSpecification(), + expectedUnits: []specter.Unit{ + mockFile.genericUnit(), }, expectedError: require.NoError, }, @@ -106,8 +106,8 @@ func TestHCLGenericSpecLoader_Load(t *testing.T) { }, }, then: then{ - expectedSpecifications: nil, - expectedError: RequireErrorWithCode(specter.UnsupportedSourceErrorCode), + expectedUnits: nil, + expectedError: RequireErrorWithCode(specter.UnsupportedSourceErrorCode), }, }, { @@ -121,12 +121,12 @@ con st = var o }, }, then: then{ - expectedSpecifications: nil, - expectedError: RequireErrorWithCode(InvalidHCLErrorCode), + expectedUnits: nil, + expectedError: RequireErrorWithCode(InvalidHCLErrorCode), }, }, { - name: "WHEN a spec type without name THEN an error should be returned", + name: "WHEN a unit type without name THEN an error should be returned", when: when{ source: specter.Source{ Data: []byte(` @@ -137,8 +137,8 @@ block { }, }, then: then{ - expectedSpecifications: nil, - expectedError: RequireErrorWithCode(InvalidHCLErrorCode), + expectedUnits: nil, + expectedError: RequireErrorWithCode(InvalidHCLErrorCode), }, }, // ATTRIBUTES @@ -155,8 +155,8 @@ specType "specName" { }, }, then: then{ - expectedSpecifications: nil, - expectedError: RequireErrorWithCode(InvalidHCLErrorCode), + expectedUnits: nil, + expectedError: RequireErrorWithCode(InvalidHCLErrorCode), }, }, { @@ -174,36 +174,36 @@ specType "specName" { }, }, then: then{ - expectedSpecifications: nil, - expectedError: RequireErrorWithCode(InvalidHCLErrorCode), + expectedUnits: nil, + expectedError: RequireErrorWithCode(InvalidHCLErrorCode), }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := NewHCLGenericSpecLoader() + l := NewHCLGenericUnitLoader() - actualSpecifications, err := l.Load(tt.when.source) + actualUnits, err := l.Load(tt.when.source) if tt.then.expectedError != nil { tt.then.expectedError(t, err) } else { require.NoError(t, err) } - assert.Equal(t, tt.then.expectedSpecifications, actualSpecifications) + assert.Equal(t, tt.then.expectedUnits, actualUnits) }) } } // CUSTOM CONFIG FILES // -func TestHCLSpecLoader_Load(t *testing.T) { +func TestHCLUnitLoader_Load(t *testing.T) { type when struct { source specter.Source } type then struct { - expectedSpecifications []specter.Specification - expectedError require.ErrorAssertionFunc + expectedUnits []specter.Unit + expectedError require.ErrorAssertionFunc } mockFile := HclConfigMock{} @@ -222,8 +222,8 @@ func TestHCLSpecLoader_Load(t *testing.T) { }, }, then: then{ - expectedSpecifications: nil, - expectedError: require.NoError, + expectedUnits: nil, + expectedError: require.NoError, }, }, { @@ -234,8 +234,8 @@ func TestHCLSpecLoader_Load(t *testing.T) { }, }, then: then{ - expectedSpecifications: nil, - expectedError: RequireErrorWithCode(specter.UnsupportedSourceErrorCode), + expectedUnits: nil, + expectedError: RequireErrorWithCode(specter.UnsupportedSourceErrorCode), }, }, { @@ -249,36 +249,36 @@ con st = var o }, }, then: then{ - expectedSpecifications: nil, - expectedError: RequireErrorWithCode(InvalidHCLErrorCode), + expectedUnits: nil, + expectedError: RequireErrorWithCode(InvalidHCLErrorCode), }, }, { - name: "WHEN valid hcl file THEN return specifications", + name: "WHEN valid hcl file THEN return units", when: when{ source: mockFile.source(), }, then: then{ - expectedSpecifications: []specter.Specification{ - mockFile.genericSpecification(), + expectedUnits: []specter.Unit{ + mockFile.genericUnit(), }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := NewHCLSpecLoader(func() HCLFileConfig { + l := NewHCLUnitLoader(func() HCLFileConfig { return &HclConfigMock{} }) - actualSpecifications, err := l.Load(tt.when.source) + actualUnits, err := l.Load(tt.when.source) if tt.then.expectedError != nil { tt.then.expectedError(t, err) } else { require.NoError(t, err) } - assert.Equal(t, tt.then.expectedSpecifications, actualSpecifications) + assert.Equal(t, tt.then.expectedUnits, actualUnits) }) } } @@ -307,26 +307,26 @@ service "specter" { `) } -func (m *HclConfigMock) Specifications() []specter.Specification { - return []specter.Specification{ - m.genericSpecification(), +func (m *HclConfigMock) Units() []specter.Unit { + return []specter.Unit{ + m.genericUnit(), } } -func (m *HclConfigMock) genericSpecification() *GenericSpecification { - spec := NewGenericSpecification("specter", "service", m.source()) +func (m *HclConfigMock) genericUnit() *GenericUnit { + unit := NewGenericUnit("specter", "service", m.source()) - spec.Attributes = append(spec.Attributes, GenericSpecAttribute{ + unit.Attributes = append(unit.Attributes, GenericUnitAttribute{ Name: "image", Value: GenericValue{ Value: cty.StringVal("specter:1.0.0"), }, }) - spec.Attributes = append(spec.Attributes, GenericSpecAttribute{ + unit.Attributes = append(unit.Attributes, GenericUnitAttribute{ Name: "dev", Value: ObjectValue{ Type: "environment", - Attributes: []GenericSpecAttribute{ + Attributes: []GenericUnitAttribute{ { Name: "MYSQL_ROOT_PASSWORD", Value: GenericValue{ @@ -336,7 +336,7 @@ func (m *HclConfigMock) genericSpecification() *GenericSpecification { }, }, }) - return spec + return unit } func (m *HclConfigMock) source() specter.Source { diff --git a/pkg/specterutils/linting.go b/pkg/specterutils/linting.go index 55a86c8..ec28bce 100644 --- a/pkg/specterutils/linting.go +++ b/pkg/specterutils/linting.go @@ -24,8 +24,8 @@ import ( const LinterResultArtifactID = "_linting_processor_results" -// UndefinedSpecificationName constant used to test against undefined SpecificationName. -const UndefinedSpecificationName specter.SpecificationName = "" +// UndefinedUnitName constant used to test against undefined UnitName. +const UndefinedUnitName specter.UnitName = "" const LintingErrorCode = "specter.spec_processing.linting_error" @@ -37,10 +37,10 @@ const ( ) type LintingProcessor struct { - linters []SpecificationLinter + linters []UnitLinter } -func NewLintingProcessor(linters ...SpecificationLinter) *LintingProcessor { +func NewLintingProcessor(linters ...UnitLinter) *LintingProcessor { return &LintingProcessor{linters: linters} } @@ -49,10 +49,10 @@ func (l LintingProcessor) Name() string { } func (l LintingProcessor) Process(ctx specter.ProcessingContext) (artifacts []specter.Artifact, err error) { - linter := CompositeSpecificationLinter(l.linters...) - ctx.Logger.Info("\nLinting specifications ...") + linter := CompositeUnitLinter(l.linters...) + ctx.Logger.Info("\nLinting units ...") - lr := linter.Lint(ctx.Specifications) + lr := linter.Lint(ctx.Units) artifacts = append(artifacts, lr) @@ -70,7 +70,7 @@ func (l LintingProcessor) Process(ctx specter.ProcessingContext) (artifacts []sp } if !lr.HasWarnings() && !lr.HasErrors() { - ctx.Logger.Success("Specifications linted successfully.") + ctx.Logger.Success("Units linted successfully.") } return artifacts, err @@ -131,40 +131,40 @@ func (s LinterResultSet) HasWarnings() bool { return len(s.Warnings()) != 0 } -// SpecificationLinter represents a function responsible for linting specifications. -type SpecificationLinter interface { - Lint(specifications specter.SpecificationGroup) LinterResultSet +// UnitLinter represents a function responsible for linting units. +type UnitLinter interface { + Lint(units specter.UnitGroup) LinterResultSet } -// SpecificationLinterFunc implementation of a SpecificationLinter that relies on a func -type SpecificationLinterFunc func(specifications specter.SpecificationGroup) LinterResultSet +// UnitLinterFunc implementation of a UnitLinter that relies on a func +type UnitLinterFunc func(units specter.UnitGroup) LinterResultSet -func (l SpecificationLinterFunc) Lint(specifications specter.SpecificationGroup) LinterResultSet { - return l(specifications) +func (l UnitLinterFunc) Lint(units specter.UnitGroup) LinterResultSet { + return l(units) } -// CompositeSpecificationLinter A Composite linter is responsible for running multiple linters as one. -func CompositeSpecificationLinter(linters ...SpecificationLinter) SpecificationLinterFunc { - return func(specifications specter.SpecificationGroup) LinterResultSet { +// CompositeUnitLinter A Composite linter is responsible for running multiple linters as one. +func CompositeUnitLinter(linters ...UnitLinter) UnitLinterFunc { + return func(units specter.UnitGroup) LinterResultSet { var result LinterResultSet for _, linter := range linters { - lr := linter.Lint(specifications) + lr := linter.Lint(units) result = append(result, lr...) } return result } } -// SpecificationMustNotHaveUndefinedNames ensures that no specification has an undefined name -func SpecificationMustNotHaveUndefinedNames(severity LinterResultSeverity) SpecificationLinterFunc { - return func(specifications specter.SpecificationGroup) LinterResultSet { +// UnitMustNotHaveUndefinedNames ensures that no unit has an undefined name +func UnitMustNotHaveUndefinedNames(severity LinterResultSeverity) UnitLinterFunc { + return func(units specter.UnitGroup) LinterResultSet { var result LinterResultSet - for _, s := range specifications { - if s.Name() == UndefinedSpecificationName { + for _, u := range units { + if u.Name() == UndefinedUnitName { result = append(result, LinterResult{ Severity: severity, - Message: fmt.Sprintf("specification at %q has an undefined name", s.Source().Location), + Message: fmt.Sprintf("unit at %q has an undefined name", u.Source().Location), }) } } @@ -173,20 +173,20 @@ func SpecificationMustNotHaveUndefinedNames(severity LinterResultSeverity) Speci } } -// SpecificationsMustHaveUniqueNames ensures that names are unique amongst specifications. -func SpecificationsMustHaveUniqueNames(severity LinterResultSeverity) SpecificationLinterFunc { - return func(specifications specter.SpecificationGroup) LinterResultSet { +// UnitsMustHaveUniqueNames ensures that names are unique amongst units. +func UnitsMustHaveUniqueNames(severity LinterResultSeverity) UnitLinterFunc { + return func(units specter.UnitGroup) LinterResultSet { var result LinterResultSet - // Where key is the type FilePath and the array contains all the specification file locations where it was encountered. - encounteredNames := map[specter.SpecificationName][]string{} + // Where key is the type FilePath and the array contains all the unit file locations where it was encountered. + encounteredNames := map[specter.UnitName][]string{} - for _, s := range specifications { - if _, found := encounteredNames[s.Name()]; found { - encounteredNames[s.Name()] = append(encounteredNames[s.Name()], s.Source().Location) + for _, u := range units { + if _, found := encounteredNames[u.Name()]; found { + encounteredNames[u.Name()] = append(encounteredNames[u.Name()], u.Source().Location) } else { - encounteredNames[s.Name()] = []string{s.Source().Location} + encounteredNames[u.Name()] = []string{u.Source().Location} } } @@ -205,7 +205,7 @@ func SpecificationsMustHaveUniqueNames(severity LinterResultSeverity) Specificat result = append(result, LinterResult{ Severity: severity, Message: fmt.Sprintf( - "duplicate specification name detected for %q in the following file(s): %s", + "duplicate unit name detected for %q in the following file(s): %s", name, strings.Join(fileNames, ", "), ), @@ -217,15 +217,15 @@ func SpecificationsMustHaveUniqueNames(severity LinterResultSeverity) Specificat } } -// SpecificationsMustHaveDescriptionAttribute ensures that all specifications have a description. -func SpecificationsMustHaveDescriptionAttribute(severity LinterResultSeverity) SpecificationLinterFunc { - return func(specifications specter.SpecificationGroup) LinterResultSet { +// UnitsMustHaveDescriptionAttribute ensures that all units have a description. +func UnitsMustHaveDescriptionAttribute(severity LinterResultSeverity) UnitLinterFunc { + return func(units specter.UnitGroup) LinterResultSet { var result LinterResultSet - for _, s := range specifications { - if s.Description() == "" { + for _, u := range units { + if u.Description() == "" { result = append(result, LinterResult{ Severity: severity, - Message: fmt.Sprintf("specification %q at location %q does not have a description", s.Name(), s.Source().Location), + Message: fmt.Sprintf("unit %q at location %q does not have a description", u.Name(), u.Source().Location), }) } } @@ -233,35 +233,35 @@ func SpecificationsMustHaveDescriptionAttribute(severity LinterResultSeverity) S } } -// SpecificationsDescriptionsMustStartWithACapitalLetter ensures that specification descriptions start with a capital letter. -func SpecificationsDescriptionsMustStartWithACapitalLetter(severity LinterResultSeverity) SpecificationLinterFunc { - return func(specifications specter.SpecificationGroup) LinterResultSet { +// UnitsDescriptionsMustStartWithACapitalLetter ensures that unit descriptions start with a capital letter. +func UnitsDescriptionsMustStartWithACapitalLetter(severity LinterResultSeverity) UnitLinterFunc { + return func(units specter.UnitGroup) LinterResultSet { var result LinterResultSet - for _, s := range specifications { - if s.Description() != "" { - firstLetter := rune(s.Description()[0]) + for _, u := range units { + if u.Description() != "" { + firstLetter := rune(u.Description()[0]) if unicode.IsUpper(firstLetter) { continue } } result = append(result, LinterResult{ Severity: severity, - Message: fmt.Sprintf("the description of specification %q at location %q does not start with a capital letter", s.Name(), s.Source().Location), + Message: fmt.Sprintf("the description of unit %q at location %q does not start with a capital letter", u.Name(), u.Source().Location), }) } return result } } -// SpecificationsDescriptionsMustEndWithPeriod ensures that specification descriptions end with a period. -func SpecificationsDescriptionsMustEndWithPeriod(severity LinterResultSeverity) SpecificationLinterFunc { - return func(specifications specter.SpecificationGroup) LinterResultSet { +// UnitsDescriptionsMustEndWithPeriod ensures that unit descriptions end with a period. +func UnitsDescriptionsMustEndWithPeriod(severity LinterResultSeverity) UnitLinterFunc { + return func(units specter.UnitGroup) LinterResultSet { var result LinterResultSet - for _, s := range specifications { - if !strings.HasSuffix(s.Description(), ".") { + for _, u := range units { + if !strings.HasSuffix(u.Description(), ".") { result = append(result, LinterResult{ Severity: severity, - Message: fmt.Sprintf("the description of specification %q at location %q does not end with a period", s.Name(), s.Source().Location), + Message: fmt.Sprintf("the description of unit %q at location %q does not end with a period", u.Name(), u.Source().Location), }) } } diff --git a/pkg/specterutils/linting_test.go b/pkg/specterutils/linting_test.go index e2010dc..e8ed99f 100644 --- a/pkg/specterutils/linting_test.go +++ b/pkg/specterutils/linting_test.go @@ -23,18 +23,18 @@ import ( "testing" ) -func TestSpecificationsDescriptionsMustStartWithACapitalLetter(t *testing.T) { +func TestUnitsDescriptionsMustStartWithACapitalLetter(t *testing.T) { tests := []struct { name string - given specter.SpecificationGroup + given specter.UnitGroup then LinterResultSet }{ { - name: "GIVEN specification starting with an upper case letter THEN return empty result set", - given: specter.SpecificationGroup{ - &GenericSpecification{ + name: "GIVEN unit starting with an upper case letter THEN return empty result set", + given: specter.UnitGroup{ + &GenericUnit{ name: "test", - Attributes: []GenericSpecAttribute{ + Attributes: []GenericUnitAttribute{ { Name: "description", Value: GenericValue{cty.StringVal("It starts with UPPERCASE")}, @@ -44,11 +44,11 @@ func TestSpecificationsDescriptionsMustStartWithACapitalLetter(t *testing.T) { }, }, { - name: "GIVEN specification starting with lower case letter THEN return error", - given: specter.SpecificationGroup{ - &GenericSpecification{ + name: "GIVEN unit starting with lower case letter THEN return error", + given: specter.UnitGroup{ + &GenericUnit{ name: "test", - Attributes: []GenericSpecAttribute{ + Attributes: []GenericUnitAttribute{ { Name: "description", Value: GenericValue{cty.StringVal("it starts with lowercase")}, @@ -59,32 +59,32 @@ func TestSpecificationsDescriptionsMustStartWithACapitalLetter(t *testing.T) { then: LinterResultSet{ { Severity: ErrorSeverity, - Message: "the description of specification \"test\" at location \"\" does not start with a capital letter", + Message: "the description of unit \"test\" at location \"\" does not start with a capital letter", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - linter := SpecificationsDescriptionsMustStartWithACapitalLetter(ErrorSeverity) + linter := UnitsDescriptionsMustStartWithACapitalLetter(ErrorSeverity) result := linter.Lint(tt.given) require.Equal(t, tt.then, result) }) } } -func TestSpecificationsDescriptionsMustEndWithPeriod(t *testing.T) { +func TestUnitsDescriptionsMustEndWithPeriod(t *testing.T) { tests := []struct { name string - given specter.SpecificationGroup + given specter.UnitGroup then LinterResultSet }{ { - name: "GIVEN specification ending with period THEN return empty result set", - given: specter.SpecificationGroup{ - &GenericSpecification{ + name: "GIVEN unit ending with period THEN return empty result set", + given: specter.UnitGroup{ + &GenericUnit{ name: "test", - Attributes: []GenericSpecAttribute{ + Attributes: []GenericUnitAttribute{ { Name: "description", Value: GenericValue{cty.StringVal("it ends with period.")}, @@ -94,11 +94,11 @@ func TestSpecificationsDescriptionsMustEndWithPeriod(t *testing.T) { }, }, { - name: "GIVEN specification not ending with period THEN return error", - given: specter.SpecificationGroup{ - &GenericSpecification{ + name: "GIVEN unit not ending with period THEN return error", + given: specter.UnitGroup{ + &GenericUnit{ name: "test", - Attributes: []GenericSpecAttribute{ + Attributes: []GenericUnitAttribute{ { Name: "description", Value: GenericValue{cty.StringVal("it starts with lowercase")}, @@ -109,32 +109,32 @@ func TestSpecificationsDescriptionsMustEndWithPeriod(t *testing.T) { then: LinterResultSet{ { Severity: ErrorSeverity, - Message: "the description of specification \"test\" at location \"\" does not end with a period", + Message: "the description of unit \"test\" at location \"\" does not end with a period", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - linter := SpecificationsDescriptionsMustEndWithPeriod(ErrorSeverity) + linter := UnitsDescriptionsMustEndWithPeriod(ErrorSeverity) result := linter.Lint(tt.given) require.Equal(t, tt.then, result) }) } } -func TestSpecificationsMustHaveDescriptionAttribute(t *testing.T) { +func TestUnitsMustHaveDescriptionAttribute(t *testing.T) { tests := []struct { name string - given specter.SpecificationGroup + given specter.UnitGroup then LinterResultSet }{ { - name: "GIVEN specification with a description THEN return empty result set", - given: specter.SpecificationGroup{ - &GenericSpecification{ + name: "GIVEN unit with a description THEN return empty result set", + given: specter.UnitGroup{ + &GenericUnit{ name: "test", - Attributes: []GenericSpecAttribute{ + Attributes: []GenericUnitAttribute{ { Name: "description", Value: GenericValue{cty.StringVal("I have a description")}, @@ -144,25 +144,25 @@ func TestSpecificationsMustHaveDescriptionAttribute(t *testing.T) { }, }, { - name: "GIVEN specification with no description ", - given: specter.SpecificationGroup{ - &GenericSpecification{ + name: "GIVEN unit with no description ", + given: specter.UnitGroup{ + &GenericUnit{ name: "test", }, }, then: LinterResultSet{ { Severity: ErrorSeverity, - Message: "specification \"test\" at location \"\" does not have a description", + Message: "unit \"test\" at location \"\" does not have a description", }, }, }, { - name: "GIVEN specification with an empty description THEN return error", - given: specter.SpecificationGroup{ - &GenericSpecification{ + name: "GIVEN unit with an empty description THEN return error", + given: specter.UnitGroup{ + &GenericUnit{ name: "test", - Attributes: []GenericSpecAttribute{ + Attributes: []GenericUnitAttribute{ { Name: "description", Value: GenericValue{cty.StringVal("")}, @@ -173,106 +173,106 @@ func TestSpecificationsMustHaveDescriptionAttribute(t *testing.T) { then: LinterResultSet{ { Severity: ErrorSeverity, - Message: "specification \"test\" at location \"\" does not have a description", + Message: "unit \"test\" at location \"\" does not have a description", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - linter := SpecificationsMustHaveDescriptionAttribute(ErrorSeverity) + linter := UnitsMustHaveDescriptionAttribute(ErrorSeverity) result := linter.Lint(tt.given) require.Equal(t, tt.then, result) }) } } -func TestSpecificationsMustHaveUniqueNames(t *testing.T) { +func TestUnitsMustHaveUniqueNames(t *testing.T) { tests := []struct { name string - given specter.SpecificationGroup + given specter.UnitGroup then LinterResultSet }{ { - name: "GIVEN specifications with unique names THEN return empty result set", - given: specter.SpecificationGroup{ - &GenericSpecification{ + name: "GIVEN units with unique names THEN return empty result set", + given: specter.UnitGroup{ + &GenericUnit{ name: "test", }, - &GenericSpecification{ + &GenericUnit{ name: "test2", }, }, }, { - name: "GIVEN specifications with non-unique names THEN return error", - given: specter.SpecificationGroup{ - &GenericSpecification{ + name: "GIVEN units with non-unique names THEN return error", + given: specter.UnitGroup{ + &GenericUnit{ name: "test", }, - &GenericSpecification{ + &GenericUnit{ name: "test", }, }, then: LinterResultSet{ { Severity: ErrorSeverity, - Message: "duplicate specification name detected for \"test\" in the following file(s): ", + Message: "duplicate unit name detected for \"test\" in the following file(s): ", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - linter := SpecificationsMustHaveUniqueNames(ErrorSeverity) + linter := UnitsMustHaveUniqueNames(ErrorSeverity) result := linter.Lint(tt.given) require.Equal(t, tt.then, result) }) } } -func TestSpecificationMustNotHaveUndefinedNames(t *testing.T) { +func TestUnitMustNotHaveUndefinedNames(t *testing.T) { tests := []struct { name string - given specter.SpecificationGroup + given specter.UnitGroup then LinterResultSet }{ { - name: "GIVEN specification with a name THEN return empty result set", - given: specter.SpecificationGroup{ - &GenericSpecification{ + name: "GIVEN unit with a name THEN return empty result set", + given: specter.UnitGroup{ + &GenericUnit{ name: "test", }, }, }, { - name: "GIVEN specification with no name THEN return error ", - given: specter.SpecificationGroup{ - &GenericSpecification{ + name: "GIVEN unit with no name THEN return error ", + given: specter.UnitGroup{ + &GenericUnit{ name: "", }, }, then: LinterResultSet{ { Severity: ErrorSeverity, - Message: "specification at \"\" has an undefined name", + Message: "unit at \"\" has an undefined name", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - linter := SpecificationMustNotHaveUndefinedNames(ErrorSeverity) + linter := UnitMustNotHaveUndefinedNames(ErrorSeverity) result := linter.Lint(tt.given) require.Equal(t, tt.then, result) }) } } -func TestCompositeSpecificationLinter(t *testing.T) { +func TestCompositeUnitLinter(t *testing.T) { type args struct { - linters []SpecificationLinter - specifications specter.SpecificationGroup + linters []UnitLinter + units specter.UnitGroup } tests := []struct { name string @@ -280,17 +280,17 @@ func TestCompositeSpecificationLinter(t *testing.T) { then LinterResultSet }{ { - name: "GIVEN valid specifications THEN return empty result set", + name: "GIVEN valid units THEN return empty result set", given: args{ - linters: []SpecificationLinter{ - SpecificationMustNotHaveUndefinedNames(ErrorSeverity), - SpecificationsDescriptionsMustStartWithACapitalLetter(ErrorSeverity), - SpecificationsDescriptionsMustEndWithPeriod(ErrorSeverity), + linters: []UnitLinter{ + UnitMustNotHaveUndefinedNames(ErrorSeverity), + UnitsDescriptionsMustStartWithACapitalLetter(ErrorSeverity), + UnitsDescriptionsMustEndWithPeriod(ErrorSeverity), }, - specifications: specter.SpecificationGroup{ - &GenericSpecification{ + units: specter.UnitGroup{ + &GenericUnit{ name: "test", - Attributes: []GenericSpecAttribute{ + Attributes: []GenericUnitAttribute{ { Name: "description", Value: GenericValue{cty.StringVal("This is a Description.")}, @@ -301,15 +301,15 @@ func TestCompositeSpecificationLinter(t *testing.T) { }, }, { - name: "GIVEN invalid specifications THEN return empty result set", + name: "GIVEN invalid units THEN return empty result set", given: args{ - linters: []SpecificationLinter{ - SpecificationMustNotHaveUndefinedNames(ErrorSeverity), - SpecificationsDescriptionsMustStartWithACapitalLetter(ErrorSeverity), - SpecificationsDescriptionsMustEndWithPeriod(ErrorSeverity), + linters: []UnitLinter{ + UnitMustNotHaveUndefinedNames(ErrorSeverity), + UnitsDescriptionsMustStartWithACapitalLetter(ErrorSeverity), + UnitsDescriptionsMustEndWithPeriod(ErrorSeverity), }, - specifications: specter.SpecificationGroup{ - &GenericSpecification{ + units: specter.UnitGroup{ + &GenericUnit{ name: "", }, }, @@ -317,24 +317,24 @@ func TestCompositeSpecificationLinter(t *testing.T) { then: LinterResultSet{ { Severity: ErrorSeverity, - Message: "specification at \"\" has an undefined name", + Message: "unit at \"\" has an undefined name", }, { Severity: ErrorSeverity, - Message: "the description of specification \"\" at location \"\" does not start with a capital letter", + Message: "the description of unit \"\" at location \"\" does not start with a capital letter", }, { Severity: ErrorSeverity, - Message: "the description of specification \"\" at location \"\" does not end with a period", + Message: "the description of unit \"\" at location \"\" does not end with a period", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - linter := CompositeSpecificationLinter(tt.given.linters...) - result := linter.Lint(tt.given.specifications) + linter := CompositeUnitLinter(tt.given.linters...) + result := linter.Lint(tt.given.units) require.Equal(t, tt.then, result) }) } @@ -478,7 +478,7 @@ func TestLintingProcessor_Name(t *testing.T) { func TestLintingProcessor_Process(t *testing.T) { type args struct { - linters []SpecificationLinter + linters []UnitLinter ctx specter.ProcessingContext } tests := []struct { @@ -501,16 +501,16 @@ func TestLintingProcessor_Process(t *testing.T) { expectedError: nil, }, { - name: "GIVEN a processing context with specifications that raise warnings THEN return a processing artifact with the result set", + name: "GIVEN a processing context with units that raise warnings THEN return a processing artifact with the result set", given: args{ - linters: []SpecificationLinter{ - SpecificationLinterFunc(func(specifications specter.SpecificationGroup) LinterResultSet { + linters: []UnitLinter{ + UnitLinterFunc(func(units specter.UnitGroup) LinterResultSet { return LinterResultSet{{Severity: WarningSeverity, Message: "a warning"}} }), }, ctx: specter.ProcessingContext{ - Specifications: []specter.Specification{NewGenericSpecification("spec", "spec_type", specter.Source{})}, - Logger: specter.NewDefaultLogger(specter.DefaultLoggerConfig{}), + Units: []specter.Unit{NewGenericUnit("unit", "spec_type", specter.Source{})}, + Logger: specter.NewDefaultLogger(specter.DefaultLoggerConfig{}), }, }, then: []specter.Artifact{ @@ -520,14 +520,14 @@ func TestLintingProcessor_Process(t *testing.T) { { name: "GIVEN a processing context that will raise errors THEN return errors", given: args{ - linters: []SpecificationLinter{ - SpecificationLinterFunc(func(specifications specter.SpecificationGroup) LinterResultSet { + linters: []UnitLinter{ + UnitLinterFunc(func(units specter.UnitGroup) LinterResultSet { return LinterResultSet{{Severity: ErrorSeverity, Message: assert.AnError.Error()}} }), }, ctx: specter.ProcessingContext{ - Specifications: []specter.Specification{NewGenericSpecification("spec", "spec_type", specter.Source{})}, - Logger: specter.NewDefaultLogger(specter.DefaultLoggerConfig{}), + Units: []specter.Unit{NewGenericUnit("unit", "spec_type", specter.Source{})}, + Logger: specter.NewDefaultLogger(specter.DefaultLoggerConfig{}), }, }, then: []specter.Artifact{ @@ -538,8 +538,8 @@ func TestLintingProcessor_Process(t *testing.T) { { name: "GIVEN a processing context that will raise both errors and warnings THEN return errors and warnings", given: args{ - linters: []SpecificationLinter{ - SpecificationLinterFunc(func(specifications specter.SpecificationGroup) LinterResultSet { + linters: []UnitLinter{ + UnitLinterFunc(func(units specter.UnitGroup) LinterResultSet { return LinterResultSet{ { Severity: ErrorSeverity, Message: assert.AnError.Error(), @@ -551,8 +551,8 @@ func TestLintingProcessor_Process(t *testing.T) { }), }, ctx: specter.ProcessingContext{ - Specifications: []specter.Specification{NewGenericSpecification("spec", "spec_type", specter.Source{})}, - Logger: specter.NewDefaultLogger(specter.DefaultLoggerConfig{}), + Units: []specter.Unit{NewGenericUnit("unit", "spec_type", specter.Source{})}, + Logger: specter.NewDefaultLogger(specter.DefaultLoggerConfig{}), }, }, then: []specter.Artifact{ diff --git a/pkg/specterutils/specversion_test.go b/pkg/specterutils/specversion_test.go deleted file mode 100644 index ea3d637..0000000 --- a/pkg/specterutils/specversion_test.go +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright 2024 Morébec -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package specterutils - -import ( - "github.com/morebec/specter/pkg/specter" - "github.com/stretchr/testify/require" - "testing" -) - -// Mock implementations for the Specification and HasVersion interfaces - -var _ specter.Specification = (*mockSpecification)(nil) - -type mockSpecification struct { - name specter.SpecificationName - description string - source specter.Source - version SpecificationVersion - typeName specter.SpecificationType -} - -func (m *mockSpecification) Name() specter.SpecificationName { - return m.name -} - -func (m *mockSpecification) Type() specter.SpecificationType { - return m.typeName -} - -func (m *mockSpecification) Description() string { - return m.description -} - -func (m *mockSpecification) SetSource(s specter.Source) { - m.source = s -} - -func (m *mockSpecification) Source() specter.Source { - return m.source -} - -func (m *mockSpecification) Version() SpecificationVersion { - return m.version -} - -func TestHasVersionMustHaveAVersionLinter(t *testing.T) { - tests := []struct { - name string - when specter.SpecificationGroup - expectedResults LinterResultSet - givenSeverity LinterResultSeverity - }{ - { - name: "WHEN some specification does not implement HasVersion, THEN ignore said specification", - when: specter.SpecificationGroup{ - &mockSpecification{name: "spec1", version: "v1"}, - NewGenericSpecification("not-versioned", "spec", specter.Source{}), - }, - givenSeverity: WarningSeverity, - expectedResults: LinterResultSet(nil), - }, - { - name: "WHEN all specifications have a version THEN return no warnings or errors", - when: specter.SpecificationGroup{ - &mockSpecification{name: "spec1", version: "v1"}, - &mockSpecification{name: "spec2", version: "v2"}, - }, - givenSeverity: WarningSeverity, - expectedResults: LinterResultSet(nil), - }, - { - name: "WHEN one specification is missing a version and severity is Warning THEN return a warning", - when: specter.SpecificationGroup{ - &mockSpecification{name: "spec1", version: "v1"}, - &mockSpecification{name: "spec2", version: ""}, - }, - givenSeverity: WarningSeverity, - expectedResults: LinterResultSet{ - { - Severity: WarningSeverity, - Message: `specification "spec2" at "" should have a version`, - }, - }, - }, - { - name: "WHEN one specification is missing a version and severity is error THEN return an error", - when: specter.SpecificationGroup{ - &mockSpecification{name: "spec1", version: "v1"}, - &mockSpecification{name: "spec2", version: ""}, - }, - givenSeverity: ErrorSeverity, - expectedResults: LinterResultSet{ - { - Severity: ErrorSeverity, - Message: `specification "spec2" at "" should have a version`, - }, - }, - }, - { - name: "multiple specifications are missing versions", - when: specter.SpecificationGroup{ - &mockSpecification{name: "spec1", version: ""}, - &mockSpecification{name: "spec2", version: ""}, - }, - givenSeverity: ErrorSeverity, - expectedResults: LinterResultSet{ - { - Severity: ErrorSeverity, - Message: `specification "spec1" at "" should have a version`, - }, - { - Severity: ErrorSeverity, - Message: `specification "spec2" at "" should have a version`, - }, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - linter := HasVersionMustHaveAVersionLinter(tt.givenSeverity) - results := linter.Lint(tt.when) - require.Equal(t, tt.expectedResults, results) - }) - } -} diff --git a/pkg/specterutils/specversion.go b/pkg/specterutils/unitversion.go similarity index 64% rename from pkg/specterutils/specversion.go rename to pkg/specterutils/unitversion.go index b77ef15..6734fd3 100644 --- a/pkg/specterutils/specversion.go +++ b/pkg/specterutils/unitversion.go @@ -19,33 +19,33 @@ import ( "github.com/morebec/specter/pkg/specter" ) -type SpecificationVersion string +type UnitVersion string type HasVersion interface { - specter.Specification + specter.Unit - Version() SpecificationVersion + Version() UnitVersion } -func HasVersionMustHaveAVersionLinter(severity LinterResultSeverity) SpecificationLinter { - return SpecificationLinterFunc(func(specifications specter.SpecificationGroup) LinterResultSet { +func HasVersionMustHaveAVersionLinter(severity LinterResultSeverity) UnitLinter { + return UnitLinterFunc(func(units specter.UnitGroup) LinterResultSet { var r LinterResultSet - specs := specifications.Select(func(s specter.Specification) bool { - if _, ok := s.(HasVersion); ok { + specs := units.Select(func(u specter.Unit) bool { + if _, ok := u.(HasVersion); ok { return true } return false }) - for _, spec := range specs { - s := spec.(HasVersion) - if s.Version() != "" { + for _, unit := range specs { + u := unit.(HasVersion) + if u.Version() != "" { continue } r = append(r, LinterResult{ Severity: severity, - Message: fmt.Sprintf("specification %q at %q should have a version", spec.Name(), spec.Source().Location), + Message: fmt.Sprintf("unit %q at %q should have a version", unit.Name(), unit.Source().Location), }) } return r diff --git a/pkg/specterutils/unitversion_test.go b/pkg/specterutils/unitversion_test.go new file mode 100644 index 0000000..bdfcd3d --- /dev/null +++ b/pkg/specterutils/unitversion_test.go @@ -0,0 +1,139 @@ +// Copyright 2024 Morébec +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package specterutils + +import ( + "github.com/morebec/specter/pkg/specter" + "github.com/stretchr/testify/require" + "testing" +) + +// Mock implementations for the Unit and HasVersion interfaces + +var _ specter.Unit = (*mockUnit)(nil) + +type mockUnit struct { + name specter.UnitName + description string + source specter.Source + version UnitVersion + typeName specter.UnitType +} + +func (m *mockUnit) Name() specter.UnitName { + return m.name +} + +func (m *mockUnit) Type() specter.UnitType { + return m.typeName +} + +func (m *mockUnit) Description() string { + return m.description +} + +func (m *mockUnit) SetSource(s specter.Source) { + m.source = s +} + +func (m *mockUnit) Source() specter.Source { + return m.source +} + +func (m *mockUnit) Version() UnitVersion { + return m.version +} + +func TestHasVersionMustHaveAVersionLinter(t *testing.T) { + tests := []struct { + name string + when specter.UnitGroup + expectedResults LinterResultSet + givenSeverity LinterResultSeverity + }{ + { + name: "WHEN some unit does not implement HasVersion, THEN ignore said unit", + when: specter.UnitGroup{ + &mockUnit{name: "spec1", version: "v1"}, + NewGenericUnit("not-versioned", "unit", specter.Source{}), + }, + givenSeverity: WarningSeverity, + expectedResults: LinterResultSet(nil), + }, + { + name: "WHEN all units have a version THEN return no warnings or errors", + when: specter.UnitGroup{ + &mockUnit{name: "spec1", version: "v1"}, + &mockUnit{name: "spec2", version: "v2"}, + }, + givenSeverity: WarningSeverity, + expectedResults: LinterResultSet(nil), + }, + { + name: "WHEN one unit is missing a version and severity is Warning THEN return a warning", + when: specter.UnitGroup{ + &mockUnit{name: "spec1", version: "v1"}, + &mockUnit{name: "spec2", version: ""}, + }, + givenSeverity: WarningSeverity, + expectedResults: LinterResultSet{ + { + Severity: WarningSeverity, + Message: `unit "spec2" at "" should have a version`, + }, + }, + }, + { + name: "WHEN one unit is missing a version and severity is error THEN return an error", + when: specter.UnitGroup{ + &mockUnit{name: "spec1", version: "v1"}, + &mockUnit{name: "spec2", version: ""}, + }, + givenSeverity: ErrorSeverity, + expectedResults: LinterResultSet{ + { + Severity: ErrorSeverity, + Message: `unit "spec2" at "" should have a version`, + }, + }, + }, + { + name: "multiple units are missing versions", + when: specter.UnitGroup{ + &mockUnit{name: "spec1", version: ""}, + &mockUnit{name: "spec2", version: ""}, + }, + givenSeverity: ErrorSeverity, + expectedResults: LinterResultSet{ + { + Severity: ErrorSeverity, + Message: `unit "spec1" at "" should have a version`, + }, + { + Severity: ErrorSeverity, + Message: `unit "spec2" at "" should have a version`, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + linter := HasVersionMustHaveAVersionLinter(tt.givenSeverity) + results := linter.Lint(tt.when) + require.Equal(t, tt.expectedResults, results) + }) + } +}