diff options
| author | mo khan <mo@mokhan.ca> | 2025-07-22 17:35:49 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-07-22 17:35:49 -0600 |
| commit | 20ef0d92694465ac86b550df139e8366a0a2b4fa (patch) | |
| tree | 3f14589e1ce6eb9306a3af31c3a1f9e1af5ed637 /vendor/github.com/authzed/spicedb/pkg/diff | |
| parent | 44e0d272c040cdc53a98b9f1dc58ae7da67752e6 (diff) | |
feat: connect to spicedb
Diffstat (limited to 'vendor/github.com/authzed/spicedb/pkg/diff')
5 files changed, 1043 insertions, 0 deletions
diff --git a/vendor/github.com/authzed/spicedb/pkg/diff/caveats/diff.go b/vendor/github.com/authzed/spicedb/pkg/diff/caveats/diff.go new file mode 100644 index 0000000..74e196b --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/diff/caveats/diff.go @@ -0,0 +1,164 @@ +package caveats + +import ( + "bytes" + + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" + + caveattypes "github.com/authzed/spicedb/pkg/caveats/types" + "github.com/authzed/spicedb/pkg/genutil/mapz" + nspkg "github.com/authzed/spicedb/pkg/namespace" + core "github.com/authzed/spicedb/pkg/proto/core/v1" +) + +// DeltaType defines the type of caveat deltas. +type DeltaType string + +const ( + // CaveatAdded indicates that the caveat was newly added/created. + CaveatAdded DeltaType = "caveat-added" + + // CaveatRemoved indicates that the caveat was removed. + CaveatRemoved DeltaType = "caveat-removed" + + // CaveatCommentsChanged indicates that the comment(s) on the caveat were changed. + CaveatCommentsChanged DeltaType = "caveat-comments-changed" + + // AddedParameter indicates that the parameter was added to the caveat. + AddedParameter DeltaType = "added-parameter" + + // RemovedParameter indicates that the parameter was removed from the caveat. + RemovedParameter DeltaType = "removed-parameter" + + // ParameterTypeChanged indicates that the type of the parameter was changed. + ParameterTypeChanged DeltaType = "parameter-type-changed" + + // CaveatExpressionChanged indicates that the expression of the caveat has changed. + CaveatExpressionChanged DeltaType = "expression-has-changed" +) + +// Diff holds the diff between two caveats. +type Diff struct { + existing *core.CaveatDefinition + updated *core.CaveatDefinition + deltas []Delta +} + +// Deltas returns the deltas between the two caveats. +func (cd Diff) Deltas() []Delta { + return cd.deltas +} + +// Delta holds a single change of a caveat. +type Delta struct { + // Type is the type of this delta. + Type DeltaType + + // ParameterName is the name of the parameter to which this delta applies, if any. + ParameterName string + + // PreviousType is the previous type of the parameter changed, if any. + PreviousType *core.CaveatTypeReference + + // CurrentType is the current type of the parameter changed, if any. + CurrentType *core.CaveatTypeReference +} + +// DiffCaveats performs a diff between two caveat definitions. One or both of the definitions +// can be `nil`, which will be treated as an add/remove as applicable. +func DiffCaveats(existing *core.CaveatDefinition, updated *core.CaveatDefinition, caveatTypeSet *caveattypes.TypeSet) (*Diff, error) { + // Check for the caveats themselves. + if existing == nil && updated == nil { + return &Diff{existing, updated, []Delta{}}, nil + } + + if existing != nil && updated == nil { + return &Diff{ + existing: existing, + updated: updated, + deltas: []Delta{ + { + Type: CaveatRemoved, + }, + }, + }, nil + } + + if existing == nil && updated != nil { + return &Diff{ + existing: existing, + updated: updated, + deltas: []Delta{ + { + Type: CaveatAdded, + }, + }, + }, nil + } + + deltas := make([]Delta, 0, len(existing.ParameterTypes)+len(updated.ParameterTypes)) + + // Check the caveats's comments. + existingComments := nspkg.GetComments(existing.Metadata) + updatedComments := nspkg.GetComments(updated.Metadata) + if !slices.Equal(existingComments, updatedComments) { + deltas = append(deltas, Delta{ + Type: CaveatCommentsChanged, + }) + } + + existingParameterNames := mapz.NewSet(maps.Keys(existing.ParameterTypes)...) + updatedParameterNames := mapz.NewSet(maps.Keys(updated.ParameterTypes)...) + + for _, removed := range existingParameterNames.Subtract(updatedParameterNames).AsSlice() { + deltas = append(deltas, Delta{ + Type: RemovedParameter, + ParameterName: removed, + }) + } + + for _, added := range updatedParameterNames.Subtract(existingParameterNames).AsSlice() { + deltas = append(deltas, Delta{ + Type: AddedParameter, + ParameterName: added, + }) + } + + for _, shared := range existingParameterNames.Intersect(updatedParameterNames).AsSlice() { + existingParamType := existing.ParameterTypes[shared] + updatedParamType := updated.ParameterTypes[shared] + + existingType, err := caveattypes.DecodeParameterType(caveatTypeSet, existingParamType) + if err != nil { + return nil, err + } + + updatedType, err := caveattypes.DecodeParameterType(caveatTypeSet, updatedParamType) + if err != nil { + return nil, err + } + + // Compare types. + if existingType.String() != updatedType.String() { + deltas = append(deltas, Delta{ + Type: ParameterTypeChanged, + ParameterName: shared, + PreviousType: existingParamType, + CurrentType: updatedParamType, + }) + } + } + + if !bytes.Equal(existing.SerializedExpression, updated.SerializedExpression) { + deltas = append(deltas, Delta{ + Type: CaveatExpressionChanged, + }) + } + + return &Diff{ + existing: existing, + updated: updated, + deltas: deltas, + }, nil +} diff --git a/vendor/github.com/authzed/spicedb/pkg/diff/diff.go b/vendor/github.com/authzed/spicedb/pkg/diff/diff.go new file mode 100644 index 0000000..34aa927 --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/diff/diff.go @@ -0,0 +1,170 @@ +package diff + +import ( + caveattypes "github.com/authzed/spicedb/pkg/caveats/types" + "github.com/authzed/spicedb/pkg/diff/caveats" + "github.com/authzed/spicedb/pkg/diff/namespace" + "github.com/authzed/spicedb/pkg/genutil/mapz" + core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/schemadsl/compiler" +) + +// DiffableSchema is a schema that can be diffed. +type DiffableSchema struct { + // ObjectDefinitions holds the object definitions in the schema. + ObjectDefinitions []*core.NamespaceDefinition + + // CaveatDefinitions holds the caveat definitions in the schema. + CaveatDefinitions []*core.CaveatDefinition +} + +func (ds *DiffableSchema) GetNamespace(namespaceName string) (*core.NamespaceDefinition, bool) { + for _, ns := range ds.ObjectDefinitions { + if ns.Name == namespaceName { + return ns, true + } + } + + return nil, false +} + +func (ds *DiffableSchema) GetRelation(nsName string, relationName string) (*core.Relation, bool) { + ns, ok := ds.GetNamespace(nsName) + if !ok { + return nil, false + } + + for _, relation := range ns.Relation { + if relation.Name == relationName { + return relation, true + } + } + + return nil, false +} + +func (ds *DiffableSchema) GetCaveat(caveatName string) (*core.CaveatDefinition, bool) { + for _, caveat := range ds.CaveatDefinitions { + if caveat.Name == caveatName { + return caveat, true + } + } + + return nil, false +} + +// NewDiffableSchemaFromCompiledSchema creates a new DiffableSchema from a CompiledSchema. +func NewDiffableSchemaFromCompiledSchema(compiled *compiler.CompiledSchema) DiffableSchema { + return DiffableSchema{ + ObjectDefinitions: compiled.ObjectDefinitions, + CaveatDefinitions: compiled.CaveatDefinitions, + } +} + +// SchemaDiff holds the diff between two schemas. +type SchemaDiff struct { + // AddedNamespaces are the namespaces that were added. + AddedNamespaces []string + + // RemovedNamespaces are the namespaces that were removed. + RemovedNamespaces []string + + // AddedCaveats are the caveats that were added. + AddedCaveats []string + + // RemovedCaveats are the caveats that were removed. + RemovedCaveats []string + + // ChangedNamespaces are the namespaces that were changed. + ChangedNamespaces map[string]namespace.Diff + + // ChangedCaveats are the caveats that were changed. + ChangedCaveats map[string]caveats.Diff +} + +// DiffSchemas compares two schemas and returns the diff. +func DiffSchemas(existing DiffableSchema, comparison DiffableSchema, caveatTypeSet *caveattypes.TypeSet) (*SchemaDiff, error) { + existingNamespacesByName := make(map[string]*core.NamespaceDefinition, len(existing.ObjectDefinitions)) + existingNamespaceNames := mapz.NewSet[string]() + for _, nsDef := range existing.ObjectDefinitions { + existingNamespacesByName[nsDef.Name] = nsDef + existingNamespaceNames.Add(nsDef.Name) + } + + existingCaveatsByName := make(map[string]*core.CaveatDefinition, len(existing.CaveatDefinitions)) + existingCaveatsByNames := mapz.NewSet[string]() + for _, caveatDef := range existing.CaveatDefinitions { + existingCaveatsByName[caveatDef.Name] = caveatDef + existingCaveatsByNames.Add(caveatDef.Name) + } + + comparisonNamespacesByName := make(map[string]*core.NamespaceDefinition, len(comparison.ObjectDefinitions)) + comparisonNamespaceNames := mapz.NewSet[string]() + for _, nsDef := range comparison.ObjectDefinitions { + comparisonNamespacesByName[nsDef.Name] = nsDef + comparisonNamespaceNames.Add(nsDef.Name) + } + + comparisonCaveatsByName := make(map[string]*core.CaveatDefinition, len(comparison.CaveatDefinitions)) + comparisonCaveatsByNames := mapz.NewSet[string]() + for _, caveatDef := range comparison.CaveatDefinitions { + comparisonCaveatsByName[caveatDef.Name] = caveatDef + comparisonCaveatsByNames.Add(caveatDef.Name) + } + + changedNamespaces := make(map[string]namespace.Diff, 0) + commonNamespaceNames := existingNamespaceNames.Intersect(comparisonNamespaceNames) + if err := commonNamespaceNames.ForEach(func(name string) error { + existingNamespace := existingNamespacesByName[name] + comparisonNamespace := comparisonNamespacesByName[name] + + diff, err := namespace.DiffNamespaces(existingNamespace, comparisonNamespace) + if err != nil { + return err + } + + if len(diff.Deltas()) > 0 { + changedNamespaces[name] = *diff + } + + return nil + }); err != nil { + return nil, err + } + + commonCaveatNames := existingCaveatsByNames.Intersect(comparisonCaveatsByNames) + changedCaveats := make(map[string]caveats.Diff, 0) + if err := commonCaveatNames.ForEach(func(name string) error { + existingCaveat := existingCaveatsByName[name] + comparisonCaveat := comparisonCaveatsByName[name] + + diff, err := caveats.DiffCaveats(existingCaveat, comparisonCaveat, caveatTypeSet) + if err != nil { + return err + } + + if len(diff.Deltas()) > 0 { + changedCaveats[name] = *diff + } + + return nil + }); err != nil { + return nil, err + } + + if len(changedNamespaces) == 0 { + changedNamespaces = nil + } + if len(changedCaveats) == 0 { + changedCaveats = nil + } + + return &SchemaDiff{ + AddedNamespaces: comparisonNamespaceNames.Subtract(existingNamespaceNames).AsSlice(), + RemovedNamespaces: existingNamespaceNames.Subtract(comparisonNamespaceNames).AsSlice(), + AddedCaveats: comparisonCaveatsByNames.Subtract(existingCaveatsByNames).AsSlice(), + RemovedCaveats: existingCaveatsByNames.Subtract(comparisonCaveatsByNames).AsSlice(), + ChangedNamespaces: changedNamespaces, + ChangedCaveats: changedCaveats, + }, nil +} diff --git a/vendor/github.com/authzed/spicedb/pkg/diff/doc.go b/vendor/github.com/authzed/spicedb/pkg/diff/doc.go new file mode 100644 index 0000000..ff3e1d0 --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/diff/doc.go @@ -0,0 +1,2 @@ +// Package diff contains code for things that can be diffed (e.g. namespaces and caveats). +package diff diff --git a/vendor/github.com/authzed/spicedb/pkg/diff/namespace/diff.go b/vendor/github.com/authzed/spicedb/pkg/diff/namespace/diff.go new file mode 100644 index 0000000..af50f0f --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/diff/namespace/diff.go @@ -0,0 +1,321 @@ +package namespace + +import ( + "github.com/google/go-cmp/cmp" + "golang.org/x/exp/slices" + "google.golang.org/protobuf/testing/protocmp" + + nsinternal "github.com/authzed/spicedb/internal/namespace" + "github.com/authzed/spicedb/pkg/genutil/mapz" + nspkg "github.com/authzed/spicedb/pkg/namespace" + core "github.com/authzed/spicedb/pkg/proto/core/v1" + iv1 "github.com/authzed/spicedb/pkg/proto/impl/v1" + "github.com/authzed/spicedb/pkg/schema" +) + +// DeltaType defines the type of namespace deltas. +type DeltaType string + +const ( + // NamespaceAdded indicates that the namespace was newly added/created. + NamespaceAdded DeltaType = "namespace-added" + + // NamespaceRemoved indicates that the namespace was removed. + NamespaceRemoved DeltaType = "namespace-removed" + + // NamespaceCommentsChanged indicates that the comment(s) on the namespace were changed. + NamespaceCommentsChanged DeltaType = "namespace-comments-changed" + + // AddedRelation indicates that the relation was added to the namespace. + AddedRelation DeltaType = "added-relation" + + // RemovedRelation indicates that the relation was removed from the namespace. + RemovedRelation DeltaType = "removed-relation" + + // AddedPermission indicates that the permission was added to the namespace. + AddedPermission DeltaType = "added-permission" + + // RemovedPermission indicates that the permission was removed from the namespace. + RemovedPermission DeltaType = "removed-permission" + + // ChangedPermissionImpl indicates that the implementation of the permission has changed in some + // way. + ChangedPermissionImpl DeltaType = "changed-permission-implementation" + + // ChangedPermissionComment indicates that the comment of the permission has changed in some way. + ChangedPermissionComment DeltaType = "changed-permission-comment" + + // LegacyChangedRelationImpl indicates that the implementation of the relation has changed in some + // way. This is for legacy checks and should not apply to any modern namespaces created + // via schema. + LegacyChangedRelationImpl DeltaType = "legacy-changed-relation-implementation" + + // RelationAllowedTypeAdded indicates that an allowed relation type has been added to + // the relation. + RelationAllowedTypeAdded DeltaType = "relation-allowed-type-added" + + // RelationAllowedTypeRemoved indicates that an allowed relation type has been removed from + // the relation. + RelationAllowedTypeRemoved DeltaType = "relation-allowed-type-removed" + + // ChangedRelationComment indicates that the comment of the relation has changed in some way. + ChangedRelationComment DeltaType = "changed-relation-comment" +) + +// Diff holds the diff between two namespaces. +type Diff struct { + existing *core.NamespaceDefinition + updated *core.NamespaceDefinition + deltas []Delta +} + +// Deltas returns the deltas between the two namespaces. +func (nd Diff) Deltas() []Delta { + return nd.deltas +} + +// Delta holds a single change of a namespace. +type Delta struct { + // Type is the type of this delta. + Type DeltaType + + // RelationName is the name of the relation to which this delta applies, if any. + RelationName string + + // AllowedType is the allowed relation type added or removed, if any. + AllowedType *core.AllowedRelation +} + +// DiffNamespaces performs a diff between two namespace definitions. One or both of the definitions +// can be `nil`, which will be treated as an add/remove as applicable. +func DiffNamespaces(existing *core.NamespaceDefinition, updated *core.NamespaceDefinition) (*Diff, error) { + // Check for the namespaces themselves. + if existing == nil && updated == nil { + return &Diff{existing, updated, []Delta{}}, nil + } + + if existing != nil && updated == nil { + return &Diff{ + existing: existing, + updated: updated, + deltas: []Delta{ + { + Type: NamespaceRemoved, + }, + }, + }, nil + } + + if existing == nil && updated != nil { + return &Diff{ + existing: existing, + updated: updated, + deltas: []Delta{ + { + Type: NamespaceAdded, + }, + }, + }, nil + } + + deltas := []Delta{} + + // Check the namespace's comments. + existingComments := nspkg.GetComments(existing.Metadata) + updatedComments := nspkg.GetComments(updated.Metadata) + if !slices.Equal(existingComments, updatedComments) { + deltas = append(deltas, Delta{ + Type: NamespaceCommentsChanged, + }) + } + + // Collect up relations and check. + existingRels := map[string]*core.Relation{} + existingRelNames := mapz.NewSet[string]() + + existingPerms := map[string]*core.Relation{} + existingPermNames := mapz.NewSet[string]() + + updatedRels := map[string]*core.Relation{} + updatedRelNames := mapz.NewSet[string]() + + updatedPerms := map[string]*core.Relation{} + updatedPermNames := mapz.NewSet[string]() + + for _, relation := range existing.Relation { + _, ok := existingRels[relation.Name] + if ok { + return nil, nsinternal.NewDuplicateRelationError(existing.Name, relation.Name) + } + + if isPermission(relation) { + existingPerms[relation.Name] = relation + existingPermNames.Add(relation.Name) + } else { + existingRels[relation.Name] = relation + existingRelNames.Add(relation.Name) + } + } + + for _, relation := range updated.Relation { + _, ok := updatedRels[relation.Name] + if ok { + return nil, nsinternal.NewDuplicateRelationError(updated.Name, relation.Name) + } + + if isPermission(relation) { + updatedPerms[relation.Name] = relation + updatedPermNames.Add(relation.Name) + } else { + updatedRels[relation.Name] = relation + updatedRelNames.Add(relation.Name) + } + } + + _ = existingRelNames.Subtract(updatedRelNames).ForEach(func(removed string) error { + deltas = append(deltas, Delta{ + Type: RemovedRelation, + RelationName: removed, + }) + return nil + }) + + _ = updatedRelNames.Subtract(existingRelNames).ForEach(func(added string) error { + deltas = append(deltas, Delta{ + Type: AddedRelation, + RelationName: added, + }) + return nil + }) + + _ = existingPermNames.Subtract(updatedPermNames).ForEach(func(removed string) error { + deltas = append(deltas, Delta{ + Type: RemovedPermission, + RelationName: removed, + }) + return nil + }) + + _ = updatedPermNames.Subtract(existingPermNames).ForEach(func(added string) error { + deltas = append(deltas, Delta{ + Type: AddedPermission, + RelationName: added, + }) + return nil + }) + + _ = existingPermNames.Intersect(updatedPermNames).ForEach(func(shared string) error { + existingPerm := existingPerms[shared] + updatedPerm := updatedPerms[shared] + + // Compare implementations. + if areDifferentExpressions(existingPerm.UsersetRewrite, updatedPerm.UsersetRewrite) { + deltas = append(deltas, Delta{ + Type: ChangedPermissionImpl, + RelationName: shared, + }) + } + + // Compare comments. + existingComments := nspkg.GetComments(existingPerm.Metadata) + updatedComments := nspkg.GetComments(updatedPerm.Metadata) + if !slices.Equal(existingComments, updatedComments) { + deltas = append(deltas, Delta{ + Type: ChangedPermissionComment, + RelationName: shared, + }) + } + return nil + }) + + _ = existingRelNames.Intersect(updatedRelNames).ForEach(func(shared string) error { + existingRel := existingRels[shared] + updatedRel := updatedRels[shared] + + // Compare implementations (legacy). + if areDifferentExpressions(existingRel.UsersetRewrite, updatedRel.UsersetRewrite) { + deltas = append(deltas, Delta{ + Type: LegacyChangedRelationImpl, + RelationName: shared, + }) + } + + // Compare comments. + existingComments := nspkg.GetComments(existingRel.Metadata) + updatedComments := nspkg.GetComments(updatedRel.Metadata) + if !slices.Equal(existingComments, updatedComments) { + deltas = append(deltas, Delta{ + Type: ChangedRelationComment, + RelationName: shared, + }) + } + + // Compare type information. + existingTypeInfo := existingRel.TypeInformation + if existingTypeInfo == nil { + existingTypeInfo = &core.TypeInformation{} + } + + updatedTypeInfo := updatedRel.TypeInformation + if updatedTypeInfo == nil { + updatedTypeInfo = &core.TypeInformation{} + } + + existingAllowedRels := mapz.NewSet[string]() + updatedAllowedRels := mapz.NewSet[string]() + allowedRelsBySource := map[string]*core.AllowedRelation{} + + for _, existingAllowed := range existingTypeInfo.AllowedDirectRelations { + source := schema.SourceForAllowedRelation(existingAllowed) + allowedRelsBySource[source] = existingAllowed + existingAllowedRels.Add(source) + } + + for _, updatedAllowed := range updatedTypeInfo.AllowedDirectRelations { + source := schema.SourceForAllowedRelation(updatedAllowed) + allowedRelsBySource[source] = updatedAllowed + updatedAllowedRels.Add(source) + } + + _ = existingAllowedRels.Subtract(updatedAllowedRels).ForEach(func(removed string) error { + deltas = append(deltas, Delta{ + Type: RelationAllowedTypeRemoved, + RelationName: shared, + AllowedType: allowedRelsBySource[removed], + }) + return nil + }) + + _ = updatedAllowedRels.Subtract(existingAllowedRels).ForEach(func(added string) error { + deltas = append(deltas, Delta{ + Type: RelationAllowedTypeAdded, + RelationName: shared, + AllowedType: allowedRelsBySource[added], + }) + return nil + }) + + return nil + }) + + return &Diff{ + existing: existing, + updated: updated, + deltas: deltas, + }, nil +} + +func isPermission(relation *core.Relation) bool { + return nspkg.GetRelationKind(relation) == iv1.RelationMetadata_PERMISSION +} + +func areDifferentExpressions(existing *core.UsersetRewrite, updated *core.UsersetRewrite) bool { + // Return whether the rewrites are different, ignoring the SourcePosition message type. + delta := cmp.Diff( + existing, + updated, + protocmp.Transform(), + protocmp.IgnoreMessages(&core.SourcePosition{}), + ) + return delta != "" +} diff --git a/vendor/github.com/authzed/spicedb/pkg/diff/namespace/diffexpr.go b/vendor/github.com/authzed/spicedb/pkg/diff/namespace/diffexpr.go new file mode 100644 index 0000000..359728c --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/diff/namespace/diffexpr.go @@ -0,0 +1,386 @@ +package namespace + +import ( + core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/spiceerrors" +) + +// ExpressionChangeType defines the type of expression changes. +type ExpressionChangeType string + +const ( + // ExpressionUnchanged indicates that the expression was unchanged. + ExpressionUnchanged ExpressionChangeType = "expression-unchanged" + + // ExpressionOperationChanged indicates that the operation type of the expression was changed. + ExpressionOperationChanged ExpressionChangeType = "operation-changed" + + // ExpressionChildrenChanged indicates that the children of the expression were changed. + ExpressionChildrenChanged ExpressionChangeType = "children-changed" + + // ExpressionOperationExpanded indicates that the operation type of the expression was expanded + // from a union of a single child to multiple children under a union, intersection or another + // operation. + ExpressionOperationExpanded ExpressionChangeType = "operation-expanded" +) + +// ExpressionDiff holds the diff between two expressions. +type ExpressionDiff struct { + existing *core.UsersetRewrite + updated *core.UsersetRewrite + change ExpressionChangeType + + childDiffs []*OperationDiff +} + +// Existing returns the existing expression, if any. +func (ed *ExpressionDiff) Existing() *core.UsersetRewrite { + return ed.existing +} + +// Updated returns the updated expression, if any. +func (ed *ExpressionDiff) Updated() *core.UsersetRewrite { + return ed.updated +} + +// Change returns the type of change that occurred. +func (ed *ExpressionDiff) Change() ExpressionChangeType { + return ed.change +} + +// ChildDiffs returns the child diffs, if any. +func (ed *ExpressionDiff) ChildDiffs() []*OperationDiff { + return ed.childDiffs +} + +// SetOperationChangeType defines the type of set operation changes. +type SetOperationChangeType string + +const ( + // OperationUnchanged indicates that the set operation was unchanged. + OperationUnchanged SetOperationChangeType = "operation-changed" + + // OperationAdded indicates that a set operation was added. + OperationAdded SetOperationChangeType = "operation-added" + + // OperationRemoved indicates that a set operation was removed. + OperationRemoved SetOperationChangeType = "operation-removed" + + // OperationTypeChanged indicates that the type of set operation was changed. + OperationTypeChanged SetOperationChangeType = "operation-type-changed" + + // OperationComputedUsersetChanged indicates that the computed userset of the operation was changed. + OperationComputedUsersetChanged SetOperationChangeType = "operation-computed-userset-changed" + + // OperationTuplesetChanged indicates that the tupleset of the operation was changed. + OperationTuplesetChanged SetOperationChangeType = "operation-tupleset-changed" + + // OperationChildExpressionChanged indicates that the child expression of the operation was changed. + OperationChildExpressionChanged SetOperationChangeType = "operation-child-expression-changed" +) + +// OperationDiff holds the diff between two set operations. +type OperationDiff struct { + existing *core.SetOperation_Child + updated *core.SetOperation_Child + change SetOperationChangeType + childExprDiff *ExpressionDiff +} + +// Existing returns the existing set operation, if any. +func (od *OperationDiff) Existing() *core.SetOperation_Child { + return od.existing +} + +// Updated returns the updated set operation, if any. +func (od *OperationDiff) Updated() *core.SetOperation_Child { + return od.updated +} + +// Change returns the type of change that occurred. +func (od *OperationDiff) Change() SetOperationChangeType { + return od.change +} + +// ChildExpressionDiff returns the child expression diff, if any. +func (od *OperationDiff) ChildExpressionDiff() *ExpressionDiff { + return od.childExprDiff +} + +// DiffExpressions diffs two expressions. +func DiffExpressions(existing *core.UsersetRewrite, updated *core.UsersetRewrite) (*ExpressionDiff, error) { + // Check for a difference in the operation type. + var existingType string + var existingOperation *core.SetOperation + var updatedType string + var updatedOperation *core.SetOperation + + switch t := existing.RewriteOperation.(type) { + case *core.UsersetRewrite_Union: + existingType = "union" + existingOperation = t.Union + + case *core.UsersetRewrite_Intersection: + existingType = "intersection" + existingOperation = t.Intersection + + case *core.UsersetRewrite_Exclusion: + existingType = "exclusion" + existingOperation = t.Exclusion + + default: + return nil, spiceerrors.MustBugf("unknown operation type %T", existing.RewriteOperation) + } + + switch t := updated.RewriteOperation.(type) { + case *core.UsersetRewrite_Union: + updatedType = "union" + updatedOperation = t.Union + + case *core.UsersetRewrite_Intersection: + updatedType = "intersection" + updatedOperation = t.Intersection + + case *core.UsersetRewrite_Exclusion: + updatedType = "exclusion" + updatedOperation = t.Exclusion + + default: + return nil, spiceerrors.MustBugf("unknown operation type %T", updated.RewriteOperation) + } + + childChangeKind := ExpressionChildrenChanged + if existingType != updatedType { + // If the expression has changed from a union with a single child, then + // treat this as a special case, since there wasn't really an operation + // before. + if existingType != "union" || len(existingOperation.Child) != 1 { + return &ExpressionDiff{ + existing: existing, + updated: updated, + change: ExpressionOperationChanged, + }, nil + } + + childChangeKind = ExpressionOperationExpanded + } + + childDiffs := make([]*OperationDiff, 0, abs(len(updatedOperation.Child)-len(existingOperation.Child))) + if len(existingOperation.Child) < len(updatedOperation.Child) { + for _, updatedChild := range updatedOperation.Child[len(existingOperation.Child):] { + childDiffs = append(childDiffs, &OperationDiff{ + change: OperationAdded, + updated: updatedChild, + }) + } + } + + if len(existingOperation.Child) > len(updatedOperation.Child) { + for _, existingChild := range existingOperation.Child[len(updatedOperation.Child):] { + childDiffs = append(childDiffs, &OperationDiff{ + change: OperationRemoved, + existing: existingChild, + }) + } + } + + for i := 0; i < len(existingOperation.Child) && i < len(updatedOperation.Child); i++ { + childDiff, err := compareChildren(existingOperation.Child[i], updatedOperation.Child[i]) + if err != nil { + return nil, err + } + + if childDiff.change != OperationUnchanged { + childDiffs = append(childDiffs, childDiff) + } + } + + if len(childDiffs) > 0 { + return &ExpressionDiff{ + existing: existing, + updated: updated, + change: childChangeKind, + childDiffs: childDiffs, + }, nil + } + + return &ExpressionDiff{ + existing: existing, + updated: updated, + change: ExpressionUnchanged, + }, nil +} + +func abs(i int) int { + if i < 0 { + return -i + } + return i +} + +func compareChildren(existing *core.SetOperation_Child, updated *core.SetOperation_Child) (*OperationDiff, error) { + existingType, err := typeOfSetOperationChild(existing) + if err != nil { + return nil, err + } + + updatedType, err := typeOfSetOperationChild(updated) + if err != nil { + return nil, err + } + + if existingType != updatedType { + return &OperationDiff{ + existing: existing, + updated: updated, + change: OperationTypeChanged, + }, nil + } + + switch existingType { + case "usersetrewrite": + childDiff, err := DiffExpressions(existing.GetUsersetRewrite(), updated.GetUsersetRewrite()) + if err != nil { + return nil, err + } + + if childDiff.change != ExpressionUnchanged { + return &OperationDiff{ + existing: existing, + updated: updated, + change: OperationChildExpressionChanged, + childExprDiff: childDiff, + }, nil + } + + return &OperationDiff{ + existing: existing, + updated: updated, + change: OperationUnchanged, + }, nil + + case "computed": + if existing.GetComputedUserset().Relation != updated.GetComputedUserset().Relation { + return &OperationDiff{ + existing: existing, + updated: updated, + change: OperationComputedUsersetChanged, + }, nil + } + + return &OperationDiff{ + existing: existing, + updated: updated, + change: OperationUnchanged, + }, nil + + case "_this": + return &OperationDiff{ + existing: existing, + updated: updated, + change: OperationUnchanged, + }, nil + + case "nil": + return &OperationDiff{ + existing: existing, + updated: updated, + change: OperationUnchanged, + }, nil + + case "ttu": + existingTTU := existing.GetTupleToUserset() + updatedTTU := updated.GetTupleToUserset() + + if existingTTU.GetComputedUserset().Relation != updatedTTU.GetComputedUserset().Relation { + return &OperationDiff{ + existing: existing, + updated: updated, + change: OperationComputedUsersetChanged, + }, nil + } + + if existingTTU.Tupleset.Relation != updatedTTU.Tupleset.Relation { + return &OperationDiff{ + existing: existing, + updated: updated, + change: OperationTuplesetChanged, + }, nil + } + + return &OperationDiff{ + existing: existing, + updated: updated, + change: OperationUnchanged, + }, nil + + case "anyttu": + fallthrough + + case "intersectionttu": + existingTTU := existing.GetFunctionedTupleToUserset() + updatedTTU := updated.GetFunctionedTupleToUserset() + + if existingTTU.GetComputedUserset().Relation != updatedTTU.GetComputedUserset().Relation { + return &OperationDiff{ + existing: existing, + updated: updated, + change: OperationComputedUsersetChanged, + }, nil + } + + if existingTTU.Tupleset.Relation != updatedTTU.Tupleset.Relation { + return &OperationDiff{ + existing: existing, + updated: updated, + change: OperationTuplesetChanged, + }, nil + } + + return &OperationDiff{ + existing: existing, + updated: updated, + change: OperationUnchanged, + }, nil + + default: + return nil, spiceerrors.MustBugf("unknown child type %s", existingType) + } +} + +func typeOfSetOperationChild(child *core.SetOperation_Child) (string, error) { + switch t := child.ChildType.(type) { + case *core.SetOperation_Child_XThis: + return "_this", nil + + case *core.SetOperation_Child_ComputedUserset: + return "computed", nil + + case *core.SetOperation_Child_UsersetRewrite: + return "usersetrewrite", nil + + case *core.SetOperation_Child_TupleToUserset: + return "ttu", nil + + case *core.SetOperation_Child_FunctionedTupleToUserset: + switch t.FunctionedTupleToUserset.Function { + case core.FunctionedTupleToUserset_FUNCTION_UNSPECIFIED: + return "", spiceerrors.MustBugf("function type unspecified") + + case core.FunctionedTupleToUserset_FUNCTION_ANY: + return "anyttu", nil + + case core.FunctionedTupleToUserset_FUNCTION_ALL: + return "intersectionttu", nil + + default: + return "", spiceerrors.MustBugf("unknown function type %v", t.FunctionedTupleToUserset.Function) + } + + case *core.SetOperation_Child_XNil: + return "nil", nil + + default: + return "", spiceerrors.MustBugf("unknown child type %T", t) + } +} |
