summaryrefslogtreecommitdiff
path: root/vendor/github.com/authzed/spicedb/pkg/tuple
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/authzed/spicedb/pkg/tuple')
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/tuple/comparison.go48
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/tuple/core.go132
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/tuple/doc.go2
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/tuple/hashing.go108
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/tuple/onr.go75
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/tuple/parsing.go228
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/tuple/strings.go165
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/tuple/structs.go219
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/tuple/updates.go22
-rw-r--r--vendor/github.com/authzed/spicedb/pkg/tuple/v1.go340
10 files changed, 1339 insertions, 0 deletions
diff --git a/vendor/github.com/authzed/spicedb/pkg/tuple/comparison.go b/vendor/github.com/authzed/spicedb/pkg/tuple/comparison.go
new file mode 100644
index 0000000..159edd9
--- /dev/null
+++ b/vendor/github.com/authzed/spicedb/pkg/tuple/comparison.go
@@ -0,0 +1,48 @@
+package tuple
+
+import (
+ "time"
+
+ "google.golang.org/protobuf/proto"
+
+ core "github.com/authzed/spicedb/pkg/proto/core/v1"
+)
+
+// ONREqual checks if two ObjectAndRelation instances are equal.
+func ONREqual(lhs, rhs ObjectAndRelation) bool {
+ return lhs == rhs
+}
+
+// ONREqualOrWildcard checks if an ObjectAndRelation matches another ObjectAndRelation or is a wildcard.
+func ONREqualOrWildcard(onr, target ObjectAndRelation) bool {
+ return ONREqual(onr, target) || (onr.ObjectID == PublicWildcard && onr.ObjectType == target.ObjectType)
+}
+
+// Equal returns true if the two relationships are exactly the same.
+func Equal(lhs, rhs Relationship) bool {
+ return ONREqual(lhs.Resource, rhs.Resource) && ONREqual(lhs.Subject, rhs.Subject) && caveatEqual(lhs.OptionalCaveat, rhs.OptionalCaveat) && expirationEqual(lhs.OptionalExpiration, rhs.OptionalExpiration)
+}
+
+func expirationEqual(lhs, rhs *time.Time) bool {
+ if lhs == nil && rhs == nil {
+ return true
+ }
+
+ if lhs == nil || rhs == nil {
+ return false
+ }
+
+ return lhs.Equal(*rhs)
+}
+
+func caveatEqual(lhs, rhs *core.ContextualizedCaveat) bool {
+ if lhs == nil && rhs == nil {
+ return true
+ }
+
+ if lhs == nil || rhs == nil {
+ return false
+ }
+
+ return lhs.CaveatName == rhs.CaveatName && proto.Equal(lhs.Context, rhs.Context)
+}
diff --git a/vendor/github.com/authzed/spicedb/pkg/tuple/core.go b/vendor/github.com/authzed/spicedb/pkg/tuple/core.go
new file mode 100644
index 0000000..d71a016
--- /dev/null
+++ b/vendor/github.com/authzed/spicedb/pkg/tuple/core.go
@@ -0,0 +1,132 @@
+package tuple
+
+import (
+ "time"
+
+ core "github.com/authzed/spicedb/pkg/proto/core/v1"
+ "github.com/authzed/spicedb/pkg/spiceerrors"
+)
+
+// ONRStringToCore creates an ONR from string pieces.
+func ONRStringToCore(ns, oid, rel string) *core.ObjectAndRelation {
+ spiceerrors.DebugAssert(func() bool {
+ return ns != "" && oid != "" && rel != ""
+ }, "namespace, object ID, and relation must not be empty")
+
+ return &core.ObjectAndRelation{
+ Namespace: ns,
+ ObjectId: oid,
+ Relation: rel,
+ }
+}
+
+// CoreRelationToStringWithoutCaveatOrExpiration creates a string from a core.RelationTuple without stringifying the caveat.
+func CoreRelationToStringWithoutCaveatOrExpiration(rel *core.RelationTuple) string {
+ if rel.Subject.Relation == Ellipsis {
+ return rel.ResourceAndRelation.Namespace + ":" + rel.ResourceAndRelation.ObjectId + "@" + rel.Subject.Namespace + ":" + rel.Subject.ObjectId
+ }
+
+ return rel.ResourceAndRelation.Namespace + ":" + rel.ResourceAndRelation.ObjectId + "@" + rel.Subject.Namespace + ":" + rel.Subject.ObjectId + "#" + rel.ResourceAndRelation.Relation
+}
+
+// CoreRelationToString creates a string from a core.RelationTuple.
+func CoreRelationToString(rel *core.RelationTuple) (string, error) {
+ return String(FromCoreRelationTuple(rel))
+}
+
+// MustCoreRelationToString creates a string from a core.RelationTuple and panics if it can't.
+func MustCoreRelationToString(rel *core.RelationTuple) string {
+ return MustString(FromCoreRelationTuple(rel))
+}
+
+// RRStringToCore creates a RelationReference from the string pieces.
+func RRStringToCore(namespaceName string, relationName string) *core.RelationReference {
+ spiceerrors.DebugAssert(func() bool {
+ return namespaceName != "" && relationName != ""
+ }, "namespace and relation must not be empty")
+
+ return &core.RelationReference{
+ Namespace: namespaceName,
+ Relation: relationName,
+ }
+}
+
+// FromCoreRelationTuple creates a Relationship from a core.RelationTuple.
+func FromCoreRelationTuple(rt *core.RelationTuple) Relationship {
+ spiceerrors.DebugAssert(func() bool {
+ return rt.Validate() == nil
+ }, "relation tuple must be valid")
+
+ var expiration *time.Time
+ if rt.OptionalExpirationTime != nil {
+ t := rt.OptionalExpirationTime.AsTime()
+ expiration = &t
+ }
+
+ return Relationship{
+ RelationshipReference: RelationshipReference{
+ Resource: ObjectAndRelation{
+ ObjectType: rt.ResourceAndRelation.Namespace,
+ ObjectID: rt.ResourceAndRelation.ObjectId,
+ Relation: rt.ResourceAndRelation.Relation,
+ },
+ Subject: ObjectAndRelation{
+ ObjectType: rt.Subject.Namespace,
+ ObjectID: rt.Subject.ObjectId,
+ Relation: rt.Subject.Relation,
+ },
+ },
+ OptionalCaveat: rt.Caveat,
+ OptionalExpiration: expiration,
+ }
+}
+
+// FromCoreObjectAndRelation creates an ObjectAndRelation from a core.ObjectAndRelation.
+func FromCoreObjectAndRelation(oar *core.ObjectAndRelation) ObjectAndRelation {
+ spiceerrors.DebugAssert(func() bool {
+ return oar.Validate() == nil
+ }, "object and relation must be valid")
+
+ return ObjectAndRelation{
+ ObjectType: oar.Namespace,
+ ObjectID: oar.ObjectId,
+ Relation: oar.Relation,
+ }
+}
+
+// CoreONR creates a core ObjectAndRelation from the string pieces.
+func CoreONR(namespace, objectID, relation string) *core.ObjectAndRelation {
+ spiceerrors.DebugAssert(func() bool {
+ return namespace != "" && objectID != "" && relation != ""
+ }, "namespace, object ID, and relation must not be empty")
+
+ return &core.ObjectAndRelation{
+ Namespace: namespace,
+ ObjectId: objectID,
+ Relation: relation,
+ }
+}
+
+// CoreRR creates a core RelationReference from the string pieces.
+func CoreRR(namespace, relation string) *core.RelationReference {
+ spiceerrors.DebugAssert(func() bool {
+ return namespace != "" && relation != ""
+ }, "namespace and relation must not be empty")
+
+ return &core.RelationReference{
+ Namespace: namespace,
+ Relation: relation,
+ }
+}
+
+// FromCoreRelationReference creates a RelationReference from a core.RelationReference.
+func FromCoreRelationReference(rr *core.RelationReference) RelationReference {
+ spiceerrors.DebugAssert(func() bool {
+ return rr.Validate() == nil
+ }, "relation reference must be valid")
+
+ return RelationReference{
+ ObjectType: rr.Namespace,
+ Relation: rr.Relation,
+ }
+}
diff --git a/vendor/github.com/authzed/spicedb/pkg/tuple/doc.go b/vendor/github.com/authzed/spicedb/pkg/tuple/doc.go
new file mode 100644
index 0000000..b86f0d2
--- /dev/null
+++ b/vendor/github.com/authzed/spicedb/pkg/tuple/doc.go
@@ -0,0 +1,2 @@
+// Package tuple provides ways to convert to and from proto structs to Go structs that can extend the core functionality.
+package tuple
diff --git a/vendor/github.com/authzed/spicedb/pkg/tuple/hashing.go b/vendor/github.com/authzed/spicedb/pkg/tuple/hashing.go
new file mode 100644
index 0000000..1f733d4
--- /dev/null
+++ b/vendor/github.com/authzed/spicedb/pkg/tuple/hashing.go
@@ -0,0 +1,108 @@
+package tuple
+
+import (
+ "bytes"
+ "fmt"
+ "sort"
+ "time"
+
+ "google.golang.org/protobuf/types/known/structpb"
+
+ "github.com/authzed/spicedb/pkg/spiceerrors"
+)
+
+// CanonicalBytes converts a tuple to a canonical set of bytes.
+// Can be used for hashing purposes.
+func CanonicalBytes(rel Relationship) ([]byte, error) {
+ spiceerrors.DebugAssert(rel.ValidateNotEmpty, "relationship must not be empty")
+
+ var sb bytes.Buffer
+ sb.WriteString(rel.Resource.ObjectType)
+ sb.WriteString(":")
+ sb.WriteString(rel.Resource.ObjectID)
+ sb.WriteString("#")
+ sb.WriteString(rel.Resource.Relation)
+ sb.WriteString("@")
+ sb.WriteString(rel.Subject.ObjectType)
+ sb.WriteString(":")
+ sb.WriteString(rel.Subject.ObjectID)
+ sb.WriteString("#")
+ sb.WriteString(rel.Subject.Relation)
+
+ if rel.OptionalCaveat != nil && rel.OptionalCaveat.CaveatName != "" {
+ sb.WriteString(" with ")
+ sb.WriteString(rel.OptionalCaveat.CaveatName)
+
+ if rel.OptionalCaveat.Context != nil && len(rel.OptionalCaveat.Context.Fields) > 0 {
+ sb.WriteString(":")
+ if err := writeCanonicalContext(&sb, rel.OptionalCaveat.Context); err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ if rel.OptionalExpiration != nil {
+ sb.WriteString(" with $expiration:")
+ truncated := rel.OptionalExpiration.UTC().Truncate(time.Second)
+ sb.WriteString(truncated.Format(expirationFormat))
+ }
+
+ return sb.Bytes(), nil
+}
+
+func writeCanonicalContext(sb *bytes.Buffer, context *structpb.Struct) error {
+ sb.WriteString("{")
+ for i, key := range sortedContextKeys(context.Fields) {
+ if i > 0 {
+ sb.WriteString(",")
+ }
+ sb.WriteString(key)
+ sb.WriteString(":")
+ if err := writeCanonicalContextValue(sb, context.Fields[key]); err != nil {
+ return err
+ }
+ }
+ sb.WriteString("}")
+ return nil
+}
+
+func writeCanonicalContextValue(sb *bytes.Buffer, value *structpb.Value) error {
+ switch value.Kind.(type) {
+ case *structpb.Value_NullValue:
+ sb.WriteString("null")
+ case *structpb.Value_NumberValue:
+ sb.WriteString(fmt.Sprintf("%f", value.GetNumberValue()))
+ case *structpb.Value_StringValue:
+ sb.WriteString(value.GetStringValue())
+ case *structpb.Value_BoolValue:
+ sb.WriteString(fmt.Sprintf("%t", value.GetBoolValue()))
+ case *structpb.Value_StructValue:
+ if err := writeCanonicalContext(sb, value.GetStructValue()); err != nil {
+ return err
+ }
+ case *structpb.Value_ListValue:
+ sb.WriteString("[")
+ for i, elem := range value.GetListValue().Values {
+ if i > 0 {
+ sb.WriteString(",")
+ }
+ if err := writeCanonicalContextValue(sb, elem); err != nil {
+ return err
+ }
+ }
+ sb.WriteString("]")
+ default:
+ return spiceerrors.MustBugf("unknown structpb.Value type: %T", value.Kind)
+ }
+
+ return nil
+}
+
+func sortedContextKeys(fields map[string]*structpb.Value) []string {
+ keys := make([]string, 0, len(fields))
+ for key := range fields {
+ keys = append(keys, key)
+ }
+ sort.Strings(keys)
+ return keys
+}
diff --git a/vendor/github.com/authzed/spicedb/pkg/tuple/onr.go b/vendor/github.com/authzed/spicedb/pkg/tuple/onr.go
new file mode 100644
index 0000000..83ad512
--- /dev/null
+++ b/vendor/github.com/authzed/spicedb/pkg/tuple/onr.go
@@ -0,0 +1,75 @@
+package tuple
+
+import (
+ "fmt"
+ "regexp"
+ "slices"
+)
+
+var (
+ onrRegex = regexp.MustCompile(fmt.Sprintf("^%s$", onrExpr))
+ subjectRegex = regexp.MustCompile(fmt.Sprintf("^%s$", subjectExpr))
+)
+
+var (
+ onrSubjectRelIndex = slices.Index(subjectRegex.SubexpNames(), "subjectRel")
+ onrSubjectTypeIndex = slices.Index(subjectRegex.SubexpNames(), "subjectType")
+ onrSubjectIDIndex = slices.Index(subjectRegex.SubexpNames(), "subjectID")
+ onrResourceTypeIndex = slices.Index(onrRegex.SubexpNames(), "resourceType")
+ onrResourceIDIndex = slices.Index(onrRegex.SubexpNames(), "resourceID")
+ onrResourceRelIndex = slices.Index(onrRegex.SubexpNames(), "resourceRel")
+)
+
+// ParseSubjectONR converts a string representation of a Subject ONR to an ObjectAndRelation. Unlike
+// ParseONR, this method allows for objects without relations. If an object without a relation
+// is given, the relation will be set to ellipsis.
+func ParseSubjectONR(subjectOnr string) (ObjectAndRelation, error) {
+ groups := subjectRegex.FindStringSubmatch(subjectOnr)
+ if len(groups) == 0 {
+ return ObjectAndRelation{}, fmt.Errorf("invalid subject ONR: %s", subjectOnr)
+ }
+
+ relation := Ellipsis
+ if len(groups[onrSubjectRelIndex]) > 0 {
+ relation = groups[onrSubjectRelIndex]
+ }
+
+ return ObjectAndRelation{
+ ObjectType: groups[onrSubjectTypeIndex],
+ ObjectID: groups[onrSubjectIDIndex],
+ Relation: relation,
+ }, nil
+}
+
+// MustParseSubjectONR converts a string representation of a Subject ONR to an ObjectAndRelation.
+// Panics on error.
+func MustParseSubjectONR(subjectOnr string) ObjectAndRelation {
+ parsed, err := ParseSubjectONR(subjectOnr)
+ if err != nil {
+ panic(err)
+ }
+ return parsed
+}
+
+// ParseONR converts a string representation of an ONR to an ObjectAndRelation object.
+func ParseONR(onr string) (ObjectAndRelation, error) {
+ groups := onrRegex.FindStringSubmatch(onr)
+ if len(groups) == 0 {
+ return ObjectAndRelation{}, fmt.Errorf("invalid ONR: %s", onr)
+ }
+
+ return ObjectAndRelation{
+ ObjectType: groups[onrResourceTypeIndex],
+ ObjectID: groups[onrResourceIDIndex],
+ Relation: groups[onrResourceRelIndex],
+ }, nil
+}
+
+// MustParseONR converts a string representation of an ONR to an ObjectAndRelation object. Panics on error.
+func MustParseONR(onr string) ObjectAndRelation {
+ parsed, err := ParseONR(onr)
+ if err != nil {
+ panic(err)
+ }
+ return parsed
+}
diff --git a/vendor/github.com/authzed/spicedb/pkg/tuple/parsing.go b/vendor/github.com/authzed/spicedb/pkg/tuple/parsing.go
new file mode 100644
index 0000000..f2ed7d2
--- /dev/null
+++ b/vendor/github.com/authzed/spicedb/pkg/tuple/parsing.go
@@ -0,0 +1,228 @@
+package tuple
+
+import (
+ "encoding/json"
+ "fmt"
+ "maps"
+ "regexp"
+ "slices"
+ "time"
+
+ "google.golang.org/protobuf/types/known/structpb"
+
+ "github.com/jzelinskie/stringz"
+
+ core "github.com/authzed/spicedb/pkg/proto/core/v1"
+)
+
+const (
+ namespaceNameExpr = "([a-z][a-z0-9_]{1,61}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]"
+ resourceIDExpr = "([a-zA-Z0-9/_|\\-=+]{1,})"
+ subjectIDExpr = "([a-zA-Z0-9/_|\\-=+]{1,})|\\*"
+ relationExpr = "[a-z][a-z0-9_]{1,62}[a-z0-9]"
+ caveatNameExpr = "([a-z][a-z0-9_]{1,61}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]"
+)
+
+var onrExpr = fmt.Sprintf(
+ `(?P<resourceType>(%s)):(?P<resourceID>%s)#(?P<resourceRel>%s)`,
+ namespaceNameExpr,
+ resourceIDExpr,
+ relationExpr,
+)
+
+var subjectExpr = fmt.Sprintf(
+ `(?P<subjectType>(%s)):(?P<subjectID>%s)(#(?P<subjectRel>%s|\.\.\.))?`,
+ namespaceNameExpr,
+ subjectIDExpr,
+ relationExpr,
+)
+
+var (
+ caveatExpr = fmt.Sprintf(`\[(?P<caveatName>(%s))(:(?P<caveatContext>(\{(.+)\})))?\]`, caveatNameExpr)
+ expirationExpr = `\[expiration:(?P<expirationDateTime>([\d\-\.:TZ]+))\]`
+)
+
+var (
+ resourceIDRegex = regexp.MustCompile(fmt.Sprintf("^%s$", resourceIDExpr))
+ subjectIDRegex = regexp.MustCompile(fmt.Sprintf("^%s$", subjectIDExpr))
+)
+
+var parserRegex = regexp.MustCompile(
+ fmt.Sprintf(
+ `^%s@%s(%s)?(%s)?$`,
+ onrExpr,
+ subjectExpr,
+ caveatExpr,
+ expirationExpr,
+ ),
+)
+
+// ValidateResourceID ensures that the given resource ID is valid. Returns an error if not.
+func ValidateResourceID(objectID string) error {
+ if !resourceIDRegex.MatchString(objectID) {
+ return fmt.Errorf("invalid resource id; must match %s", resourceIDExpr)
+ }
+ if len(objectID) > 1024 {
+ return fmt.Errorf("invalid resource id; must be <= 1024 characters")
+ }
+
+ return nil
+}
+
+// ValidateSubjectID ensures that the given object ID (under a subject reference) is valid. Returns an error if not.
+func ValidateSubjectID(subjectID string) error {
+ if !subjectIDRegex.MatchString(subjectID) {
+ return fmt.Errorf("invalid subject id; must be alphanumeric and between 1 and 127 characters or a star for public")
+ }
+ if len(subjectID) > 1024 {
+ return fmt.Errorf("invalid resource id; must be <= 1024 characters")
+ }
+
+ return nil
+}
+
+// MustParse wraps Parse such that any failures panic rather than returning an error.
+func MustParse(relString string) Relationship {
+ parsed, err := Parse(relString)
+ if err != nil {
+ panic(err)
+ }
+ return parsed
+}
+
+var (
+ subjectRelIndex = slices.Index(parserRegex.SubexpNames(), "subjectRel")
+ caveatNameIndex = slices.Index(parserRegex.SubexpNames(), "caveatName")
+ caveatContextIndex = slices.Index(parserRegex.SubexpNames(), "caveatContext")
+ resourceIDIndex = slices.Index(parserRegex.SubexpNames(), "resourceID")
+ subjectIDIndex = slices.Index(parserRegex.SubexpNames(), "subjectID")
+ resourceTypeIndex = slices.Index(parserRegex.SubexpNames(), "resourceType")
+ resourceRelIndex = slices.Index(parserRegex.SubexpNames(), "resourceRel")
+ subjectTypeIndex = slices.Index(parserRegex.SubexpNames(), "subjectType")
+ expirationDateTimeIndex = slices.Index(parserRegex.SubexpNames(), "expirationDateTime")
+)
+
+// Parse unmarshals the string form of a Tuple and returns an error on failure,
+//
+// This function treats both missing and Ellipsis relations equally.
+func Parse(relString string) (Relationship, error) {
+ groups := parserRegex.FindStringSubmatch(relString)
+ if len(groups) == 0 {
+ return Relationship{}, fmt.Errorf("invalid relationship string")
+ }
+
+ subjectRelation := Ellipsis
+ if len(groups[subjectRelIndex]) > 0 {
+ subjectRelation = stringz.DefaultEmpty(groups[subjectRelIndex], Ellipsis)
+ }
+
+ caveatName := groups[caveatNameIndex]
+ var optionalCaveat *core.ContextualizedCaveat
+ if caveatName != "" {
+ optionalCaveat = &core.ContextualizedCaveat{
+ CaveatName: caveatName,
+ }
+
+ caveatContextString := groups[caveatContextIndex]
+ if len(caveatContextString) > 0 {
+ contextMap := make(map[string]any, 1)
+ err := json.Unmarshal([]byte(caveatContextString), &contextMap)
+ if err != nil {
+ return Relationship{}, fmt.Errorf("invalid caveat context JSON: %w", err)
+ }
+
+ caveatContext, err := structpb.NewStruct(contextMap)
+ if err != nil {
+ return Relationship{}, fmt.Errorf("invalid caveat context: %w", err)
+ }
+
+ optionalCaveat.Context = caveatContext
+ }
+ }
+
+ expirationTimeStr := groups[expirationDateTimeIndex]
+ var optionalExpiration *time.Time
+ if len(expirationTimeStr) > 0 {
+ expirationTime, err := time.Parse(expirationFormat, expirationTimeStr)
+ if err != nil {
+ return Relationship{}, fmt.Errorf("invalid expiration time: %w", err)
+ }
+
+ optionalExpiration = &expirationTime
+ }
+
+ resourceID := groups[resourceIDIndex]
+ if err := ValidateResourceID(resourceID); err != nil {
+ return Relationship{}, fmt.Errorf("invalid resource id: %w", err)
+ }
+
+ subjectID := groups[subjectIDIndex]
+ if err := ValidateSubjectID(subjectID); err != nil {
+ return Relationship{}, fmt.Errorf("invalid subject id: %w", err)
+ }
+
+ return Relationship{
+ RelationshipReference: RelationshipReference{
+ Resource: ObjectAndRelation{
+ ObjectType: groups[resourceTypeIndex],
+ ObjectID: resourceID,
+ Relation: groups[resourceRelIndex],
+ },
+ Subject: ObjectAndRelation{
+ ObjectType: groups[subjectTypeIndex],
+ ObjectID: subjectID,
+ Relation: subjectRelation,
+ },
+ },
+ OptionalCaveat: optionalCaveat,
+ OptionalExpiration: optionalExpiration,
+ }, nil
+}
+
+// MustWithExpiration adds the given expiration to the relationship. This is for testing only.
+func MustWithExpiration(rel Relationship, expiration time.Time) Relationship {
+ rel.OptionalExpiration = &expiration
+ return rel
+}
+
+// MustWithCaveat adds the given caveat name to the relationship. This is for testing only.
+func MustWithCaveat(rel Relationship, caveatName string, contexts ...map[string]any) Relationship {
+ wc, err := WithCaveat(rel, caveatName, contexts...)
+ if err != nil {
+ panic(err)
+ }
+ return wc
+}
+
+// WithCaveat adds the given caveat name to the relationship. This is for testing only.
+func WithCaveat(rel Relationship, caveatName string, contexts ...map[string]any) (Relationship, error) {
+ var context *structpb.Struct
+
+ if len(contexts) > 0 {
+ combined := map[string]any{}
+ for _, current := range contexts {
+ maps.Copy(combined, current)
+ }
+
+ contextStruct, err := structpb.NewStruct(combined)
+ if err != nil {
+ return Relationship{}, err
+ }
+ context = contextStruct
+ }
+
+ rel.OptionalCaveat = &core.ContextualizedCaveat{
+ CaveatName: caveatName,
+ Context: context,
+ }
+ return rel, nil
+}
+
+// StringToONR creates an ONR from string pieces.
+func StringToONR(ns, oid, rel string) ObjectAndRelation {
+ return ObjectAndRelation{
+ ObjectType: ns,
+ ObjectID: oid,
+ Relation: rel,
+ }
+}
diff --git a/vendor/github.com/authzed/spicedb/pkg/tuple/strings.go b/vendor/github.com/authzed/spicedb/pkg/tuple/strings.go
new file mode 100644
index 0000000..7b5a378
--- /dev/null
+++ b/vendor/github.com/authzed/spicedb/pkg/tuple/strings.go
@@ -0,0 +1,165 @@
+package tuple
+
+import (
+ "sort"
+ "strings"
+ "time"
+
+ "google.golang.org/protobuf/encoding/protojson"
+ "google.golang.org/protobuf/types/known/structpb"
+
+ core "github.com/authzed/spicedb/pkg/proto/core/v1"
+ "github.com/authzed/spicedb/pkg/spiceerrors"
+)
+
+var expirationFormat = time.RFC3339Nano
+
+// JoinRelRef joins the namespace and relation together into the same
+// format as `StringRR()`.
+func JoinRelRef(namespace, relation string) string { return namespace + "#" + relation }
+
+// MustSplitRelRef splits a string produced by `JoinRelRef()` and panics if
+// it fails.
+func MustSplitRelRef(relRef string) (namespace, relation string) {
+ var ok bool
+ namespace, relation, ok = strings.Cut(relRef, "#")
+ if !ok {
+ panic("improperly formatted relation reference")
+ }
+ return
+}
+
+// StringRR converts a RR object to a string.
+func StringRR(rr RelationReference) string {
+ return JoinRelRef(rr.ObjectType, rr.Relation)
+}
+
+// StringONR converts an ONR object to a string.
+func StringONR(onr ObjectAndRelation) string {
+ return StringONRStrings(onr.ObjectType, onr.ObjectID, onr.Relation)
+}
+
+func StringCoreRR(rr *core.RelationReference) string {
+ if rr == nil {
+ return ""
+ }
+
+ return JoinRelRef(rr.Namespace, rr.Relation)
+}
+
+// StringCoreONR converts a core ONR object to a string.
+func StringCoreONR(onr *core.ObjectAndRelation) string {
+ if onr == nil {
+ return ""
+ }
+
+ return StringONRStrings(onr.Namespace, onr.ObjectId, onr.Relation)
+}
+
+// StringONRStrings converts ONR strings to a string.
+func StringONRStrings(namespace, objectID, relation string) string {
+ if relation == Ellipsis {
+ return JoinObjectRef(namespace, objectID)
+ }
+ return JoinRelRef(JoinObjectRef(namespace, objectID), relation)
+}
+
+// StringsONRs converts ONR objects to a string slice, sorted.
+func StringsONRs(onrs []ObjectAndRelation) []string {
+ onrstrings := make([]string, 0, len(onrs))
+ for _, onr := range onrs {
+ onrstrings = append(onrstrings, StringONR(onr))
+ }
+
+ sort.Strings(onrstrings)
+ return onrstrings
+}
+
+// MustString converts a relationship to a string.
+func MustString(rel Relationship) string {
+ tplString, err := String(rel)
+ if err != nil {
+ panic(err)
+ }
+ return tplString
+}
+
+// String converts a relationship to a string.
+func String(rel Relationship) (string, error) {
+ spiceerrors.DebugAssert(rel.ValidateNotEmpty, "relationship must not be empty")
+
+ caveatString, err := StringCaveat(rel.OptionalCaveat)
+ if err != nil {
+ return "", err
+ }
+
+ expirationString, err := StringExpiration(rel.OptionalExpiration)
+ if err != nil {
+ return "", err
+ }
+
+ return StringONR(rel.Resource) + "@" + StringONR(rel.Subject) + caveatString + expirationString, nil
+}
+
+func StringExpiration(expiration *time.Time) (string, error) {
+ if expiration == nil {
+ return "", nil
+ }
+
+ return "[expiration:" + expiration.Format(expirationFormat) + "]", nil
+}
+
+// StringWithoutCaveatOrExpiration converts a relationship to a string, without its caveat or expiration included.
+func StringWithoutCaveatOrExpiration(rel Relationship) string {
+ spiceerrors.DebugAssert(rel.ValidateNotEmpty, "relationship must not be empty")
+
+ return StringONR(rel.Resource) + "@" + StringONR(rel.Subject)
+}
+
+func MustStringCaveat(caveat *core.ContextualizedCaveat) string {
+ caveatString, err := StringCaveat(caveat)
+ if err != nil {
+ panic(err)
+ }
+ return caveatString
+}
+
+// StringCaveat converts a contextualized caveat to a string. If the caveat is nil or empty, returns empty string.
+func StringCaveat(caveat *core.ContextualizedCaveat) (string, error) {
+ if caveat == nil || caveat.CaveatName == "" {
+ return "", nil
+ }
+
+ contextString, err := StringCaveatContext(caveat.Context)
+ if err != nil {
+ return "", err
+ }
+
+ if len(contextString) > 0 {
+ contextString = ":" + contextString
+ }
+
+ return "[" + caveat.CaveatName + contextString + "]", nil
+}
+
+// StringCaveatContext converts the context of a caveat to a string. If the context is nil or empty, returns an empty string.
+func StringCaveatContext(context *structpb.Struct) (string, error) {
+ if context == nil || len(context.Fields) == 0 {
+ return "", nil
+ }
+
+ contextBytes, err := protojson.MarshalOptions{
+ Multiline: false,
+ Indent: "",
+ }.Marshal(context)
+ if err != nil {
+ return "", err
+ }
+ return string(contextBytes), nil
+}
+
+// JoinObjectRef joins the namespace and the objectId together into the standard
+// format.
+//
+// This function assumes that the provided values have already been validated.
+func JoinObjectRef(namespace, objectID string) string { return namespace + ":" + objectID }
diff --git a/vendor/github.com/authzed/spicedb/pkg/tuple/structs.go b/vendor/github.com/authzed/spicedb/pkg/tuple/structs.go
new file mode 100644
index 0000000..e6cd256
--- /dev/null
+++ b/vendor/github.com/authzed/spicedb/pkg/tuple/structs.go
@@ -0,0 +1,219 @@
+package tuple
+
+import (
+ "errors"
+ "fmt"
+ "time"
+
+ "google.golang.org/protobuf/types/known/timestamppb"
+
+ core "github.com/authzed/spicedb/pkg/proto/core/v1"
+ "github.com/authzed/spicedb/pkg/spiceerrors"
+)
+
+const (
+ // Ellipsis is the Ellipsis relation in v0 style subjects.
+ Ellipsis = "..."
+
+ // PublicWildcard is the wildcard value for subject object IDs that indicates public access
+ // for the subject type.
+ PublicWildcard = "*"
+)
+
+// ObjectAndRelation represents an object and its relation.
+type ObjectAndRelation struct {
+ ObjectID string
+ ObjectType string
+ Relation string
+}
+
+const onrStructSize = 48 /* size of the struct itself */
+
+func (onr ObjectAndRelation) SizeVT() int {
+ return len(onr.ObjectID) + len(onr.ObjectType) + len(onr.Relation) + onrStructSize
+}
+
+// WithRelation returns a copy of the object and relation with the given relation.
+func (onr ObjectAndRelation) WithRelation(relation string) ObjectAndRelation {
+ onr.Relation = relation
+ return onr
+}
+
+// RelationReference returns a RelationReference for the object and relation.
+func (onr ObjectAndRelation) RelationReference() RelationReference {
+ return RelationReference{
+ ObjectType: onr.ObjectType,
+ Relation: onr.Relation,
+ }
+}
+
+// ToCoreONR converts the ObjectAndRelation to a core.ObjectAndRelation.
+func (onr ObjectAndRelation) ToCoreONR() *core.ObjectAndRelation {
+ return &core.ObjectAndRelation{
+ Namespace: onr.ObjectType,
+ ObjectId: onr.ObjectID,
+ Relation: onr.Relation,
+ }
+}
+
+func (onr ObjectAndRelation) String() string {
+ return fmt.Sprintf("%s:%s#%s", onr.ObjectType, onr.ObjectID, onr.Relation)
+}
+
+// RelationshipReference represents a reference to a relationship, i.e. those portions
+// of a relationship that are not the integrity or caveat and thus form the unique
+// identifier of the relationship.
+type RelationshipReference struct {
+ Resource ObjectAndRelation
+ Subject ObjectAndRelation
+}
+
+// Relationship represents a relationship between two objects.
+type Relationship struct {
+ OptionalCaveat *core.ContextualizedCaveat
+ OptionalExpiration *time.Time
+ OptionalIntegrity *core.RelationshipIntegrity
+
+ RelationshipReference
+}
+
+// ToCoreTuple converts the Relationship to a core.RelationTuple.
+func (r Relationship) ToCoreTuple() *core.RelationTuple {
+ var expirationTime *timestamppb.Timestamp
+ if r.OptionalExpiration != nil {
+ expirationTime = timestamppb.New(*r.OptionalExpiration)
+ }
+
+ return &core.RelationTuple{
+ ResourceAndRelation: r.Resource.ToCoreONR(),
+ Subject: r.Subject.ToCoreONR(),
+ Caveat: r.OptionalCaveat,
+ Integrity: r.OptionalIntegrity,
+ OptionalExpirationTime: expirationTime,
+ }
+}
+
+const relStructSize = 120 /* size of the struct itself */
+
+func (r Relationship) SizeVT() int {
+ size := r.Resource.SizeVT() + r.Subject.SizeVT() + relStructSize
+ if r.OptionalCaveat != nil {
+ size += r.OptionalCaveat.SizeVT()
+ }
+ return size
+}
+
+// ValidateNotEmpty returns true if the relationship is not empty.
+func (r Relationship) ValidateNotEmpty() bool {
+ return r.Resource.ObjectType != "" && r.Resource.ObjectID != "" && r.Subject.ObjectType != "" && r.Subject.ObjectID != "" && r.Resource.Relation != "" && r.Subject.Relation != ""
+}
+
+// Validate returns an error if the relationship is invalid.
+func (r Relationship) Validate() error {
+ if !r.ValidateNotEmpty() {
+ return errors.New("object and relation must not be empty")
+ }
+
+ if r.RelationshipReference.Resource.ObjectID == PublicWildcard {
+ return errors.New("invalid resource id")
+ }
+
+ return nil
+}
+
+// WithoutIntegrity returns a copy of the relationship without its integrity.
+func (r Relationship) WithoutIntegrity() Relationship {
+ r.OptionalIntegrity = nil
+ return r
+}
+
+// WithCaveat returns a copy of the relationship with the given caveat.
+func (r Relationship) WithCaveat(caveat *core.ContextualizedCaveat) Relationship {
+ r.OptionalCaveat = caveat
+ return r
+}
+
+// UpdateOperation represents the type of update to a relationship.
+type UpdateOperation int
+
+const (
+ UpdateOperationTouch UpdateOperation = iota
+ UpdateOperationCreate
+ UpdateOperationDelete
+)
+
+// RelationshipUpdate represents an update to a relationship.
+type RelationshipUpdate struct {
+ Relationship Relationship
+ Operation UpdateOperation
+}
+
+func (ru RelationshipUpdate) OperationString() string {
+ switch ru.Operation {
+ case UpdateOperationTouch:
+ return "TOUCH"
+ case UpdateOperationCreate:
+ return "CREATE"
+ case UpdateOperationDelete:
+ return "DELETE"
+ default:
+ return "unknown"
+ }
+}
+
+func (ru RelationshipUpdate) DebugString() string {
+ return fmt.Sprintf("%s(%s)", ru.OperationString(), StringWithoutCaveatOrExpiration(ru.Relationship))
+}
+
+// RelationReference represents a reference to a relation.
+type RelationReference struct {
+ ObjectType string
+ Relation string
+}
+
+// ToCoreRR converts the RelationReference to a core.RelationReference.
+func (rr RelationReference) ToCoreRR() *core.RelationReference {
+ return &core.RelationReference{
+ Namespace: rr.ObjectType,
+ Relation: rr.Relation,
+ }
+}
+
+func (rr RelationReference) RefString() string {
+ return JoinRelRef(rr.ObjectType, rr.Relation)
+}
+
+func (rr RelationReference) String() string {
+ return rr.RefString()
+}
+
+// ONR creates an ObjectAndRelation.
+func ONR(namespace, objectID, relation string) ObjectAndRelation {
+ spiceerrors.DebugAssert(func() bool {
+ return namespace != "" && objectID != "" && relation != ""
+ }, "invalid ONR: %s %s %s", namespace, objectID, relation)
+
+ return ObjectAndRelation{
+ ObjectType: namespace,
+ ObjectID: objectID,
+ Relation: relation,
+ }
+}
+
+// ONRRef creates an ObjectAndRelation reference.
+func ONRRef(namespace, objectID, relation string) *ObjectAndRelation {
+ onr := ONR(namespace, objectID, relation)
+ return &onr
+}
+
+// RR creates a RelationReference.
+func RR(namespace, relation string) RelationReference {
+ spiceerrors.DebugAssert(func() bool {
+ return namespace != "" && relation != ""
+ }, "invalid RR: %s %s", namespace, relation)
+
+ return RelationReference{
+ ObjectType: namespace,
+ Relation: relation,
+ }
+}
diff --git a/vendor/github.com/authzed/spicedb/pkg/tuple/updates.go b/vendor/github.com/authzed/spicedb/pkg/tuple/updates.go
new file mode 100644
index 0000000..73eafe9
--- /dev/null
+++ b/vendor/github.com/authzed/spicedb/pkg/tuple/updates.go
@@ -0,0 +1,22 @@
+package tuple
+
+func Create(rel Relationship) RelationshipUpdate {
+ return RelationshipUpdate{
+ Operation: UpdateOperationCreate,
+ Relationship: rel,
+ }
+}
+
+func Touch(rel Relationship) RelationshipUpdate {
+ return RelationshipUpdate{
+ Operation: UpdateOperationTouch,
+ Relationship: rel,
+ }
+}
+
+func Delete(rel Relationship) RelationshipUpdate {
+ return RelationshipUpdate{
+ Operation: UpdateOperationDelete,
+ Relationship: rel,
+ }
+}
diff --git a/vendor/github.com/authzed/spicedb/pkg/tuple/v1.go b/vendor/github.com/authzed/spicedb/pkg/tuple/v1.go
new file mode 100644
index 0000000..5a3705f
--- /dev/null
+++ b/vendor/github.com/authzed/spicedb/pkg/tuple/v1.go
@@ -0,0 +1,340 @@
+package tuple
+
+import (
+ "fmt"
+ "time"
+
+ v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
+ "github.com/jzelinskie/stringz"
+ "google.golang.org/protobuf/types/known/timestamppb"
+
+ core "github.com/authzed/spicedb/pkg/proto/core/v1"
+ "github.com/authzed/spicedb/pkg/spiceerrors"
+)
+
+// ParseV1Rel parses a string representation of a relationship into a v1.Relationship object.
+func ParseV1Rel(relString string) (*v1.Relationship, error) {
+ parsed, err := Parse(relString)
+ if err != nil {
+ return nil, err
+ }
+
+ return ToV1Relationship(parsed), nil
+}
+
+// Same as above, but panics if it cannot be parsed. Should only be used in tests.
+func MustParseV1Rel(relString string) *v1.Relationship {
+ parsed, err := Parse(relString)
+ if err != nil {
+ panic(fmt.Sprintf("could not parse relationship string: %s %s", relString, err))
+ }
+
+ return ToV1Relationship(parsed)
+}
+
+// MustV1RelString converts a relationship into a string. Will panic if
+// the Relationship does not validate.
+func MustV1RelString(rel *v1.Relationship) string {
+ if err := rel.Validate(); err != nil {
+ panic(fmt.Sprintf("invalid relationship: %#v %s", rel, err))
+ }
+ return MustV1StringRelationship(rel)
+}
+
+// StringObjectRef marshals a *v1.ObjectReference into a string.
+//
+// This function assumes that the provided values have already been validated.
+func V1StringObjectRef(ref *v1.ObjectReference) string {
+ return JoinObjectRef(ref.ObjectType, ref.ObjectId)
+}
+
+// StringSubjectRef marshals a *v1.SubjectReference into a string.
+//
+// This function assumes that the provided values have already been validated.
+func V1StringSubjectRef(ref *v1.SubjectReference) string {
+ if ref.OptionalRelation == "" {
+ return V1StringObjectRef(ref.Object)
+ }
+ return JoinRelRef(V1StringObjectRef(ref.Object), ref.OptionalRelation)
+}
+
+// MustV1StringRelationship converts a v1.Relationship to a string.
+func MustV1StringRelationship(rel *v1.Relationship) string {
+ relString, err := V1StringRelationship(rel)
+ if err != nil {
+ panic(err)
+ }
+ return relString
+}
+
+// V1StringRelationship converts a v1.Relationship to a string.
+func V1StringRelationship(rel *v1.Relationship) (string, error) {
+ if rel == nil || rel.Resource == nil || rel.Subject == nil {
+ return "", nil
+ }
+
+ caveatString, err := V1StringCaveatRef(rel.OptionalCaveat)
+ if err != nil {
+ return "", err
+ }
+
+ expirationString, err := V1StringExpiration(rel.OptionalExpiresAt)
+ if err != nil {
+ return "", err
+ }
+
+ return V1StringRelationshipWithoutCaveatOrExpiration(rel) + caveatString + expirationString, nil
+}
+
+func V1StringExpiration(expiration *timestamppb.Timestamp) (string, error) {
+ if expiration == nil {
+ return "", nil
+ }
+
+ return "[expiration:" + expiration.AsTime().Format(expirationFormat) + "]", nil
+}
+
+// V1StringRelationshipWithoutCaveatOrExpiration converts a v1.Relationship to a string, excluding any caveat.
+func V1StringRelationshipWithoutCaveatOrExpiration(rel *v1.Relationship) string {
+ if rel == nil || rel.Resource == nil || rel.Subject == nil {
+ return ""
+ }
+
+ return V1StringObjectRef(rel.Resource) + "#" + rel.Relation + "@" + V1StringSubjectRef(rel.Subject)
+}
+
+// V1StringCaveatRef converts a v1.ContextualizedCaveat to a string.
+func V1StringCaveatRef(caveat *v1.ContextualizedCaveat) (string, error) {
+ if caveat == nil || caveat.CaveatName == "" {
+ return "", nil
+ }
+
+ contextString, err := StringCaveatContext(caveat.Context)
+ if err != nil {
+ return "", err
+ }
+
+ if len(contextString) > 0 {
+ contextString = ":" + contextString
+ }
+
+ return "[" + caveat.CaveatName + contextString + "]", nil
+}
+
+// UpdateToV1RelationshipUpdate converts a RelationshipUpdate into a
+// v1.RelationshipUpdate.
+func UpdateToV1RelationshipUpdate(update RelationshipUpdate) (*v1.RelationshipUpdate, error) {
+ var op v1.RelationshipUpdate_Operation
+ switch update.Operation {
+ case UpdateOperationCreate:
+ op = v1.RelationshipUpdate_OPERATION_CREATE
+ case UpdateOperationDelete:
+ op = v1.RelationshipUpdate_OPERATION_DELETE
+ case UpdateOperationTouch:
+ op = v1.RelationshipUpdate_OPERATION_TOUCH
+ default:
+ return nil, spiceerrors.MustBugf("unknown update operation: %v", update.Operation)
+ }
+
+ return &v1.RelationshipUpdate{
+ Operation: op,
+ Relationship: ToV1Relationship(update.Relationship),
+ }, nil
+}
+
+// MustUpdateToV1RelationshipUpdate converts a RelationshipUpdate into a
+// v1.RelationshipUpdate. Panics on error.
+func MustUpdateToV1RelationshipUpdate(update RelationshipUpdate) *v1.RelationshipUpdate {
+ v1rel, err := UpdateToV1RelationshipUpdate(update)
+ if err != nil {
+ panic(err)
+ }
+
+ return v1rel
+}
+
+// UpdateFromV1RelationshipUpdate converts a RelationshipUpdate into a
+// RelationTupleUpdate.
+func UpdateFromV1RelationshipUpdate(update *v1.RelationshipUpdate) (RelationshipUpdate, error) {
+ var op UpdateOperation
+ switch update.Operation {
+ case v1.RelationshipUpdate_OPERATION_CREATE:
+ op = UpdateOperationCreate
+ case v1.RelationshipUpdate_OPERATION_DELETE:
+ op = UpdateOperationDelete
+ case v1.RelationshipUpdate_OPERATION_TOUCH:
+ op = UpdateOperationTouch
+ default:
+ return RelationshipUpdate{}, spiceerrors.MustBugf("unknown update operation: %v", update.Operation)
+ }
+
+ return RelationshipUpdate{
+ Operation: op,
+ Relationship: FromV1Relationship(update.Relationship),
+ }, nil
+}
+
+// FromV1Relationship converts a v1.Relationship into a Relationship.
+func FromV1Relationship(rel *v1.Relationship) Relationship {
+ var caveat *core.ContextualizedCaveat
+ if rel.OptionalCaveat != nil {
+ caveat = &core.ContextualizedCaveat{
+ CaveatName: rel.OptionalCaveat.CaveatName,
+ Context: rel.OptionalCaveat.Context,
+ }
+ }
+
+ var expiration *time.Time
+ if rel.OptionalExpiresAt != nil {
+ t := rel.OptionalExpiresAt.AsTime()
+ expiration = &t
+ }
+
+ return Relationship{
+ RelationshipReference: RelationshipReference{
+ Resource: ObjectAndRelation{
+ ObjectID: rel.Resource.ObjectId,
+ ObjectType: rel.Resource.ObjectType,
+ Relation: rel.Relation,
+ },
+ Subject: ObjectAndRelation{
+ ObjectID: rel.Subject.Object.ObjectId,
+ ObjectType: rel.Subject.Object.ObjectType,
+ Relation: stringz.Default(rel.Subject.OptionalRelation, Ellipsis, ""),
+ },
+ },
+ OptionalCaveat: caveat,
+ OptionalExpiration: expiration,
+ }
+}
+
+// ToV1Relationship converts a Relationship into a v1.Relationship.
+func ToV1Relationship(rel Relationship) *v1.Relationship {
+ var caveat *v1.ContextualizedCaveat
+ if rel.OptionalCaveat != nil {
+ caveat = &v1.ContextualizedCaveat{
+ CaveatName: rel.OptionalCaveat.CaveatName,
+ Context: rel.OptionalCaveat.Context,
+ }
+ }
+
+ var expiration *timestamppb.Timestamp
+ if rel.OptionalExpiration != nil {
+ expiration = timestamppb.New(*rel.OptionalExpiration)
+ }
+
+ return &v1.Relationship{
+ Resource: &v1.ObjectReference{
+ ObjectType: rel.Resource.ObjectType,
+ ObjectId: rel.Resource.ObjectID,
+ },
+ Relation: rel.Resource.Relation,
+ Subject: &v1.SubjectReference{
+ Object: &v1.ObjectReference{
+ ObjectType: rel.Subject.ObjectType,
+ ObjectId: rel.Subject.ObjectID,
+ },
+ OptionalRelation: stringz.Default(rel.Subject.Relation, "", Ellipsis),
+ },
+ OptionalCaveat: caveat,
+ OptionalExpiresAt: expiration,
+ }
+}
+
+// CopyToV1Relationship copies a Relationship into a v1.Relationship.
+func CopyToV1Relationship(rel Relationship, v1rel *v1.Relationship) {
+ v1rel.Resource.ObjectType = rel.Resource.ObjectType
+ v1rel.Resource.ObjectId = rel.Resource.ObjectID
+ v1rel.Relation = rel.Resource.Relation
+ v1rel.Subject.Object.ObjectType = rel.Subject.ObjectType
+ v1rel.Subject.Object.ObjectId = rel.Subject.ObjectID
+ v1rel.Subject.OptionalRelation = stringz.Default(rel.Subject.Relation, "", Ellipsis)
+
+ if rel.OptionalCaveat != nil {
+ if v1rel.OptionalCaveat == nil {
+ v1rel.OptionalCaveat = &v1.ContextualizedCaveat{}
+ }
+
+ v1rel.OptionalCaveat.CaveatName = rel.OptionalCaveat.CaveatName
+ v1rel.OptionalCaveat.Context = rel.OptionalCaveat.Context
+ } else {
+ v1rel.OptionalCaveat = nil
+ }
+
+ if rel.OptionalExpiration != nil {
+ v1rel.OptionalExpiresAt = timestamppb.New(*rel.OptionalExpiration)
+ } else {
+ v1rel.OptionalExpiresAt = nil
+ }
+}
+
+// UpdatesToV1RelationshipUpdates converts a slice of RelationshipUpdate into a
+// slice of v1.RelationshipUpdate.
+func UpdatesToV1RelationshipUpdates(updates []RelationshipUpdate) ([]*v1.RelationshipUpdate, error) {
+ relationshipUpdates := make([]*v1.RelationshipUpdate, 0, len(updates))
+
+ for _, update := range updates {
+ converted, err := UpdateToV1RelationshipUpdate(update)
+ if err != nil {
+ return nil, err
+ }
+
+ relationshipUpdates = append(relationshipUpdates, converted)
+ }
+
+ return relationshipUpdates, nil
+}
+
+// UpdatesFromV1RelationshipUpdates converts a slice of v1.RelationshipUpdate into a
+// slice of RelationshipUpdate.
+func UpdatesFromV1RelationshipUpdates(updates []*v1.RelationshipUpdate) ([]RelationshipUpdate, error) {
+ relationshipUpdates := make([]RelationshipUpdate, 0, len(updates))
+
+ for _, update := range updates {
+ converted, err := UpdateFromV1RelationshipUpdate(update)
+ if err != nil {
+ return nil, err
+ }
+
+ relationshipUpdates = append(relationshipUpdates, converted)
+ }
+
+ return relationshipUpdates, nil
+}
+
+// ToV1Filter converts a RelationTuple into a RelationshipFilter.
+func ToV1Filter(rel Relationship) *v1.RelationshipFilter {
+ return &v1.RelationshipFilter{
+ ResourceType: rel.Resource.ObjectType,
+ OptionalResourceId: rel.Resource.ObjectID,
+ OptionalRelation: rel.Resource.Relation,
+ OptionalSubjectFilter: SubjectONRToSubjectFilter(rel.Subject),
+ }
+}
+
+// SubjectONRToSubjectFilter converts a userset to the equivalent exact SubjectFilter.
+func SubjectONRToSubjectFilter(subject ObjectAndRelation) *v1.SubjectFilter {
+ return &v1.SubjectFilter{
+ SubjectType: subject.ObjectType,
+ OptionalSubjectId: subject.ObjectID,
+ OptionalRelation: &v1.SubjectFilter_RelationFilter{
+ Relation: stringz.Default(subject.Relation, "", Ellipsis),
+ },
+ }
+}
+
+// RelToFilter converts a Relationship into a RelationshipFilter.
+func RelToFilter(rel *v1.Relationship) *v1.RelationshipFilter {
+ return &v1.RelationshipFilter{
+ ResourceType: rel.Resource.ObjectType,
+ OptionalResourceId: rel.Resource.ObjectId,
+ OptionalRelation: rel.Relation,
+ OptionalSubjectFilter: &v1.SubjectFilter{
+ SubjectType: rel.Subject.Object.ObjectType,
+ OptionalSubjectId: rel.Subject.Object.ObjectId,
+ OptionalRelation: &v1.SubjectFilter_RelationFilter{
+ Relation: rel.Subject.OptionalRelation,
+ },
+ },
+ }
+}