diff --git a/client/building_block_definition.go b/client/building_block_definition.go index f7ec893..06f1fb3 100644 --- a/client/building_block_definition.go +++ b/client/building_block_definition.go @@ -115,6 +115,7 @@ type MeshBuildingBlockDefinitionStatus struct { LatestVersionUuid string `json:"latestVersionUuid"` LatestReleasedVersion *int64 `json:"latestReleasedVersion"` LatestReleasedVersionUuid *string `json:"latestReleasedVersionUuid"` + RedactedForNonOwnerAccess bool `json:"redactedForNonOwnerAccess"` } type MeshBuildingBlockDefinition struct { diff --git a/client/building_block_definition_version.go b/client/building_block_definition_version.go index 98a6c77..50952c5 100644 --- a/client/building_block_definition_version.go +++ b/client/building_block_definition_version.go @@ -209,7 +209,7 @@ type MeshBuildingBlockDefinitionVersionSpec struct { type MeshBuildingBlockDefinitionVersionStatus struct { State MeshBuildingBlockDefinitionVersionState `json:"state" tfsdk:"state"` - UsageCount int64 `json:"usageCount" tfsdk:"usage_count"` + UsageCount *int64 `json:"usageCount,omitzero" tfsdk:"usage_count"` } type MeshBuildingBlockDefinitionVersion struct { diff --git a/client/building_block_definition_version_implementation.go b/client/building_block_definition_version_implementation.go index 7c2182d..ceef537 100644 --- a/client/building_block_definition_version_implementation.go +++ b/client/building_block_definition_version_implementation.go @@ -3,10 +3,10 @@ package client import ( "encoding/json/v2" "fmt" - "reflect" "github.com/meshcloud/meshstack-cli/client/types" "github.com/meshcloud/meshstack-cli/client/types/enum" + "github.com/meshcloud/meshstack-cli/client/types/variant" ) type MeshBuildingBlockImplementationType string @@ -74,32 +74,31 @@ type MeshBuildingBlockDefinitionImplementation struct { Terraform *MeshBuildingBlockDefinitionTerraformImplementation `json:"terraform,omitzero" tfsdk:"terraform"` } -func (m MeshBuildingBlockDefinitionImplementation) InferTypeFromNonNilField() (result enum.Entry[MeshBuildingBlockImplementationType]) { - setResultIfNotNil := func(implType enum.Entry[MeshBuildingBlockImplementationType], v any) { - // Manual implementation is an empty struct, so carefully check v for nilness using reflection! - if !reflect.ValueOf(v).IsZero() { - if len(result) > 0 && result != implType { - panic(fmt.Errorf("inferred implementation type %s but already set to %s", implType, result)) - } - result = implType - } - } - setResultIfNotNil(MeshBuildingBlockImplementationTypeManual, m.Manual) - setResultIfNotNil(MeshBuildingBlockImplementationTypeTerraform, m.Terraform) - setResultIfNotNil(MeshBuildingBlockImplementationTypeGithubWorkflows, m.GithubWorkflows) - setResultIfNotNil(MeshBuildingBlockImplementationTypeGitlabPipeline, m.GitlabPipeline) - setResultIfNotNil(MeshBuildingBlockImplementationTypeAzureDevOpsPipeline, m.AzureDevOpsPipeline) - if len(result) == 0 { - panic("cannot infer implementation type") +// InferType derives the implementation type from the one variant that is set. A version without any variant +// is what meshStack answers a workspace that may only consume the definition. +func (m MeshBuildingBlockDefinitionImplementation) InferType() (enum.Entry[MeshBuildingBlockImplementationType], error) { + result, err := variant.InferType( + variant.NewCandidate(MeshBuildingBlockImplementationTypeManual, m.Manual != nil), + variant.NewCandidate(MeshBuildingBlockImplementationTypeTerraform, m.Terraform != nil), + variant.NewCandidate(MeshBuildingBlockImplementationTypeGithubWorkflows, m.GithubWorkflows != nil), + variant.NewCandidate(MeshBuildingBlockImplementationTypeGitlabPipeline, m.GitlabPipeline != nil), + variant.NewCandidate(MeshBuildingBlockImplementationTypeAzureDevOpsPipeline, m.AzureDevOpsPipeline != nil), + ) + if err != nil { + return "", fmt.Errorf("cannot infer implementation type: %w", err) } - return + return result, nil } func (m MeshBuildingBlockDefinitionImplementation) MarshalJSON() ([]byte, error) { type wrapped MeshBuildingBlockDefinitionImplementation w := wrapped(m) if len(w.Type) == 0 { - w.Type = m.InferTypeFromNonNilField() + inferred, err := m.InferType() + if err != nil { + return nil, err + } + w.Type = inferred } return json.Marshal(w, wireCompatibility) } diff --git a/client/building_block_definition_version_implementation_test.go b/client/building_block_definition_version_implementation_test.go new file mode 100644 index 0000000..1443970 --- /dev/null +++ b/client/building_block_definition_version_implementation_test.go @@ -0,0 +1,22 @@ +package client + +import ( + "encoding/json/v2" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMeshBuildingBlockDefinitionImplementation_InferType(t *testing.T) { + t.Run("an empty manual struct counts as set", func(t *testing.T) { + got, err := MeshBuildingBlockDefinitionImplementation{Manual: &MeshBuildingBlockDefinitionManualImplementation{}}.InferType() + require.NoError(t, err) + assert.Equal(t, MeshBuildingBlockImplementationTypeManual, got) + }) + + t.Run("marshalling without a variant reports the error", func(t *testing.T) { + _, err := json.Marshal(MeshBuildingBlockDefinitionImplementation{}) + require.ErrorContains(t, err, "cannot infer implementation type: no variant is set") + }) +} diff --git a/client/integration_config.go b/client/integration_config.go index 874553e..46d6d07 100644 --- a/client/integration_config.go +++ b/client/integration_config.go @@ -3,10 +3,10 @@ package client import ( "encoding/json/v2" "fmt" - "reflect" "github.com/meshcloud/meshstack-cli/client/types" "github.com/meshcloud/meshstack-cli/client/types/enum" + "github.com/meshcloud/meshstack-cli/client/types/variant" ) type MeshIntegrationConfigType string @@ -57,30 +57,32 @@ type MeshIntegrationConfig struct { EntraId *MeshIntegrationEntraIdConfig `json:"entraid,omitzero" tfsdk:"entraid"` } -func (m MeshIntegrationConfig) InferTypeFromNonNilField() (result enum.Entry[MeshIntegrationConfigType]) { - setResultIfNotNil := func(implType enum.Entry[MeshIntegrationConfigType], v any) { - if !reflect.ValueOf(v).IsZero() { - if len(result) > 0 && result != implType { - panic(fmt.Errorf("inferred config type %s but already set to %s", implType, result)) - } - result = implType - } - } - setResultIfNotNil(MeshIntegrationConfigTypeGithub, m.Github) - setResultIfNotNil(MeshIntegrationConfigTypeGitlab, m.Gitlab) - setResultIfNotNil(MeshIntegrationConfigTypeAzureDevops, m.AzureDevops) - setResultIfNotNil(MeshIntegrationConfigTypeEntraId, m.EntraId) - if len(result) == 0 { - panic("cannot infer config type") +func (m MeshIntegrationConfig) InferType() (enum.Entry[MeshIntegrationConfigType], error) { + result, err := variant.InferType( + variant.NewCandidate(MeshIntegrationConfigTypeGithub, m.Github != nil), + variant.NewCandidate(MeshIntegrationConfigTypeGitlab, m.Gitlab != nil), + variant.NewCandidate(MeshIntegrationConfigTypeAzureDevops, m.AzureDevops != nil), + variant.NewCandidate(MeshIntegrationConfigTypeEntraId, m.EntraId != nil), + ) + if err != nil { + return "", fmt.Errorf("cannot infer integration config type: %w", err) } - return + return result, nil } func (m MeshIntegrationConfig) MarshalJSON() ([]byte, error) { // Using wrapped type avoids calling MarshalJSON recursively! type wrapped MeshIntegrationConfig w := wrapped(m) - w.Type = m.InferTypeFromNonNilField() + // Built-in integrations (replicator, metering) come with a type but no variant, so the type is only + // inferred when it is missing. + if len(w.Type) == 0 { + inferred, err := m.InferType() + if err != nil { + return nil, err + } + w.Type = inferred + } return json.Marshal(w, wireCompatibility) } diff --git a/client/integration_config_test.go b/client/integration_config_test.go new file mode 100644 index 0000000..25ba5ee --- /dev/null +++ b/client/integration_config_test.go @@ -0,0 +1,15 @@ +package client + +import ( + "encoding/json/v2" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMeshIntegrationConfig_MarshalJSON(t *testing.T) { + t.Run("without a variant reports the error", func(t *testing.T) { + _, err := json.Marshal(MeshIntegrationConfig{}) + require.ErrorContains(t, err, "cannot infer integration config type: no variant is set") + }) +} diff --git a/client/types/variant/infer.go b/client/types/variant/infer.go new file mode 100644 index 0000000..d9e9a7b --- /dev/null +++ b/client/types/variant/infer.go @@ -0,0 +1,36 @@ +package variant + +import ( + "errors" + "fmt" + + "github.com/meshcloud/meshstack-cli/client/types/enum" +) + +// A Candidate is one variant of a type that holds exactly one of several variants, one field each. +type Candidate[T ~string] struct { + Type enum.Entry[T] + IsSet bool +} + +func NewCandidate[T ~string](typ enum.Entry[T], isSet bool) Candidate[T] { + return Candidate[T]{Type: typ, IsSet: isSet} +} + +// InferType returns the type of the one candidate that is set. +func InferType[T ~string](candidates ...Candidate[T]) (enum.Entry[T], error) { + var result enum.Entry[T] + for _, candidate := range candidates { + if !candidate.IsSet { + continue + } + if len(result) > 0 { + return "", fmt.Errorf("more than one variant is set: %s and %s", result, candidate.Type) + } + result = candidate.Type + } + if len(result) == 0 { + return "", errors.New("no variant is set") + } + return result, nil +} diff --git a/client/types/variant/infer_test.go b/client/types/variant/infer_test.go new file mode 100644 index 0000000..8a93741 --- /dev/null +++ b/client/types/variant/infer_test.go @@ -0,0 +1,53 @@ +package variant + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/meshcloud/meshstack-cli/client/types/enum" +) + +type shape string + +const ( + circle enum.Entry[shape] = "circle" + square enum.Entry[shape] = "square" +) + +type shapeHolder struct { + Circle *struct{} + Square *struct{ Side int } +} + +func (s shapeHolder) inferType() (enum.Entry[shape], error) { + return InferType( + NewCandidate(circle, s.Circle != nil), + NewCandidate(square, s.Square != nil), + ) +} + +func TestInferType(t *testing.T) { + t.Run("an empty struct counts as set", func(t *testing.T) { + got, err := shapeHolder{Circle: &struct{}{}}.inferType() + require.NoError(t, err) + assert.Equal(t, circle, got) + }) + + t.Run("one variant", func(t *testing.T) { + got, err := shapeHolder{Square: &struct{ Side int }{Side: 2}}.inferType() + require.NoError(t, err) + assert.Equal(t, square, got) + }) + + t.Run("no variant", func(t *testing.T) { + _, err := shapeHolder{}.inferType() + require.EqualError(t, err, "no variant is set") + }) + + t.Run("several variants", func(t *testing.T) { + _, err := shapeHolder{Circle: &struct{}{}, Square: &struct{ Side int }{}}.inferType() + require.EqualError(t, err, "more than one variant is set: circle and square") + }) +}