pkg/task/taskid/taskid.go (82 lines of code) (raw):
// Copyright 2024 Google LLC
//
// 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 taskid
import (
"fmt"
"strings"
)
// TaskImplementationID represents a unique identifier for a specific task implementation.
// It is formatted as "ReferenceID#ImplementationHash" where ReferenceID identifies the type of task
// and ImplementationHash identifies the specific implementation of that task type.
// TaskReference is an ID used to refer to tasks in dependencies. It consists of
// only the ReferenceID part without the implementation hash suffix.
// For example, "foo.bar#qux" is a TaskImplementationID where "foo.bar" is the ReferenceID
// and "qux" is the ImplementationHash. Multiple implementations can share the same ReferenceID
// but must produce the same type of output (e.g., "foo.bar#qux" and "foo.bar#baz").
// This dual-ID system allows tasks to depend on a common interface (via TaskReference)
// while the actual implementation used (TaskImplementationID) can vary based on context.
// UntypedTaskReference defines the interface for task references without type information.
// This allows the task system to handle references generically when exact types are not needed.
type UntypedTaskReference interface {
// String returns the string representation of the reference ID.
String() string
// ReferenceIDString returns the reference ID portion without any implementation hash.
ReferenceIDString() string
// isTaskReference is a marker to prevent TaskImplementationID implementing this interface.
isTaskReference() bool
}
// TaskReference defines a typed reference to a task that produces a specific result type.
// The type parameter ensures that dependencies between tasks maintain type safety.
type TaskReference[TaskResult any] interface {
UntypedTaskReference
// GetZeroValue returns a zero value of the TaskResult type.
// This is used to maintain type safety by ensuring TaskReference[A] and TaskReference[B]
// are considered different types when A and B are different.
GetZeroValue() TaskResult
}
// UntypedTaskImplementationID defines the interface for task implementation IDs
// without type information. This allows the task system to handle task IDs
// generically when exact types are not needed.
type UntypedTaskImplementationID interface {
// String returns the full string representation of the task implementation ID (ReferenceID#ImplementationHash).
String() string
// ReferenceIDString returns only the reference ID portion without the implementation hash.
ReferenceIDString() string
// GetTaskImplementationHash returns the implementation-specific hash part of the ID.
GetTaskImplementationHash() string
// GetUntypedReference returns the reference ID associated with this implementation ID.
GetUntypedReference() UntypedTaskReference
}
// TaskImplementationID defines a typed implementation ID for a task that produces a specific result type.
// The type parameter ensures that implementations maintain type safety with their references.
type TaskImplementationID[TaskResult any] interface {
UntypedTaskImplementationID
// Ref returns the typed reference associated with this implementation ID.
Ref() TaskReference[TaskResult]
}
// taskReferenceImpl implements the TaskReference interface for a specific result type.
type taskReferenceImpl[TaskResult any] struct {
id string
}
// String returns the string representation of the reference ID.
func (t taskReferenceImpl[TaskResult]) String() string {
return t.id
}
// GetZeroValue returns a zero value of the TaskResult type.
// This is used to distinguish between TaskReference[A] and TaskReference[B] at the type level.
func (t taskReferenceImpl[TaskResult]) GetZeroValue() TaskResult {
return *new(TaskResult)
}
// taskImplementationIDImpl implements the TaskImplementationID interface for a specific result type.
type taskImplementationIDImpl[TaskResult any] struct {
referenceId string
implementationHash string
}
// String returns the full string representation of the implementation ID in the format "referenceId#implementationHash".
func (t taskImplementationIDImpl[TaskResult]) String() string {
return t.referenceId + "#" + t.implementationHash
}
// Ref returns a TaskReference associated with this implementation ID.
// This allows accessing the reference from the implementation, enabling type-safe dependencies.
func (t taskImplementationIDImpl[TaskResult]) Ref() TaskReference[TaskResult] {
return taskReferenceImpl[TaskResult]{id: t.referenceId}
}
// ReferenceIDString returns the reference ID portion of the task reference.
// For taskReferenceImpl, this is the same as String().
func (t taskReferenceImpl[TaskResult]) ReferenceIDString() string {
return t.String()
}
func (t taskReferenceImpl[TaskResult]) isTaskReference() bool {
return true
}
// ReferenceIDString returns only the reference ID portion of the implementation ID, without the hash.
func (t taskImplementationIDImpl[TaskResult]) ReferenceIDString() string {
return t.referenceId
}
// GetTaskImplementationHash returns the implementation-specific hash part of the ID.
// This distinguishes between different implementations of the same reference.
func (t taskImplementationIDImpl[TaskResult]) GetTaskImplementationHash() string {
return t.implementationHash
}
// GetUntypedReference returns the reference ID associated with this implementation ID as an UntypedTaskReference.
// This allows working with references without knowledge of their specific result types.
func (t taskImplementationIDImpl[TaskResult]) GetUntypedReference() UntypedTaskReference {
return t.Ref()
}
// NewTaskReference creates a new TaskReference with the specified ID.
// This function is used to create references to tasks that can be used in dependencies.
// The ID cannot contain '#' as it would be confused with an implementation hash.
// Typically used to define the interface of a task that other tasks can depend on.
func NewTaskReference[TaskResult any](id string) TaskReference[TaskResult] {
if strings.Contains(id, "#") {
panic(fmt.Sprintf("reference id %s is invalid. It cannot contain '#' in reference ID\nThis is likely a bug in the KHI task implementation or an incorrect ID was provided in the taskid definition.\nPlease report a bug at https://github.com/GoogleCloudPlatform/khi/issues", id))
}
return taskReferenceImpl[TaskResult]{id: id}
}
// NewDefaultImplementationID creates a new TaskImplementationID with the "default" implementation hash.
// This is a convenience function for creating standard task implementations.
// The ID cannot contain '#' as the function will append "#default" to create the full implementation ID.
// Typically used when there is only one common implementation of a task reference.
func NewDefaultImplementationID[TaskResult any](id string) TaskImplementationID[TaskResult] {
if strings.Contains(id, "#") {
panic(fmt.Sprintf("task id %s is invalid. It cannot contain '#' in NewDefaultImplementationID. Use NewImplementationID instead to use a custom implementation hash.\nThis is likely a bug in the KHI task implementation or an incorrect ID was provided in the taskid definition.\nPlease report a bug at https://github.com/GoogleCloudPlatform/khi/issues", id))
}
return taskImplementationIDImpl[TaskResult]{referenceId: id, implementationHash: "default"}
}
// NewImplementationID creates a new TaskImplementationID with a custom implementation hash.
// This function is used when multiple different implementations of the same task reference are needed.
// The implementation hash distinguishes between different implementations that produce the same type of result.
// For example, a log parser task could have different implementations for different log formats,
// but all implementations would share the same TaskReference.
func NewImplementationID[TaskResult any](baseReference TaskReference[TaskResult], implementationHash string) TaskImplementationID[TaskResult] {
if strings.Contains(implementationHash, "#") {
panic(fmt.Sprintf("implementation hash %s is invalid. It cannot contain '#' in NewImplementationID.\nThis is likely a bug in the KHI task implementation or an incorrect ID was provided in the taskid definition.\nPlease report a bug at https://github.com/GoogleCloudPlatform/khi/issues", implementationHash))
}
return taskImplementationIDImpl[TaskResult]{referenceId: baseReference.String(), implementationHash: implementationHash}
}
// ReinterpretTaskReference casts UntypedTaskReference to TaskReference[T]. Use this with caution.
func ReinterpretTaskReference[T any](ref UntypedTaskReference) TaskReference[T] {
return ref.(TaskReference[T])
}
// ReinterpretTaskImplementationID casts UntypedImplementationID to TaskImplementationID[T]. Use this with caution.
func ReinterpretTaskImplementationID[T any](id UntypedTaskImplementationID) TaskImplementationID[T] {
return id.(TaskImplementationID[T])
}