Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Opskit pulls the common language into one small package. A component can say:
- whether it supports active checks
- whether it supports grouped checks
- whether it supports operational commands
- what safe attributes it emits
- what safe attributes it exposes

The application still owns policy. Opskit only gives the policy a stable shape.

Expand Down
14 changes: 12 additions & 2 deletions check.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ type CheckDescriptor struct {
Attributes []Attribute `json:"attributes,omitempty"`
}

// Checker performs one operational check.
// Checker performs one active operational check.
//
// Check is an execution hook. Opskit does not schedule it, wrap it with
// timeout or panic recovery, retry it, authorize it, audit it, limit its
// concurrency, or export telemetry for it. Callers that invoke Check own those
// policies.
type Checker interface {
Check(context.Context) CheckResult
}
Expand All @@ -61,7 +66,12 @@ type NamedCheck struct {
Result CheckResult `json:"result"`
}

// CheckGroup performs a group of operational checks.
// CheckGroup performs a group of active operational checks.
//
// CheckAll is an execution hook. Opskit does not schedule it, wrap it with
// timeout or panic recovery, retry it, authorize it, audit it, limit its
// concurrency, or export telemetry for it. Callers that invoke CheckAll own
// those policies.
type CheckGroup interface {
CheckAll(context.Context) CheckSummary
}
Expand Down
30 changes: 29 additions & 1 deletion command.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ type CommandRequest struct {
Attributes []Attribute `json:"attributes,omitempty"`
}

// NewCommandRequest returns a command request with the current UTC timestamp.
//
// NewCommandRequest does not validate the command name or payload. Callers that
// need a custom RequestedAt value can construct CommandRequest directly.
func NewCommandRequest(name string, payload json.RawMessage, attrs ...Attribute) CommandRequest {
return CommandRequest{
Name: name,
Payload: cloneRawMessage(payload),
RequestedAt: nowUTC(),
Attributes: cloneAttributes(attrs),
}
}

// CommandDescriptor describes one supported operational command.
//
// Descriptors are passive metadata for presentation, documentation, and
Expand Down Expand Up @@ -62,7 +75,12 @@ type CommandResult struct {
Attributes []Attribute `json:"attributes,omitempty"`
}

// CommandHandler handles an operational command.
// CommandHandler handles an active operational command.
//
// HandleCommand is an execution hook. Opskit does not dispatch it, wrap it with
// timeout or panic recovery, retry it, authorize it, audit it, limit its
// concurrency, validate its payload, or export telemetry for it. Callers that
// invoke HandleCommand own those policies.
type CommandHandler interface {
HandleCommand(context.Context, CommandRequest) CommandResult
}
Expand All @@ -85,6 +103,16 @@ func cloneCommandDescriptors(commands []CommandDescriptor) []CommandDescriptor {
return cloned
}

func cloneRawMessage(payload json.RawMessage) json.RawMessage {
if len(payload) == 0 {
return nil
}

cloned := make(json.RawMessage, len(payload))
copy(cloned, payload)
return cloned
}

// CommandHandlerFunc adapts a function into a CommandHandler.
type CommandHandlerFunc func(context.Context, CommandRequest) CommandResult

Expand Down
44 changes: 44 additions & 0 deletions command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,50 @@ func TestCommandRequestPayloadRawMessageRoundTrip(t *testing.T) {
}
}

func TestNewCommandRequest(t *testing.T) {
payload := json.RawMessage(`{"force":true}`)
attrs := []Attribute{
Attr("requested_by", "admin"),
}

request := NewCommandRequest("cache/refresh", payload, attrs...)
payload[9] = 'f'
attrs[0] = Attr("requested_by", "mutated")

if request.Name != "cache/refresh" {
t.Fatalf("Name = %q, want cache/refresh", request.Name)
}
if string(request.Payload) != `{"force":true}` {
t.Fatalf("Payload = %s, want original payload", request.Payload)
}
if request.RequestedAt == nil {
t.Fatal("RequestedAt is nil")
}
if request.RequestedAt.Location() != time.UTC {
t.Fatalf("RequestedAt location = %q, want UTC", request.RequestedAt.Location())
}
if len(request.Attributes) != 1 {
t.Fatalf("Attributes length = %d, want 1", len(request.Attributes))
}
if request.Attributes[0] != Attr("requested_by", "admin") {
t.Fatalf("Attributes[0] = %+v, want requested_by admin", request.Attributes[0])
}
}

func TestNewCommandRequestWithEmptyPayload(t *testing.T) {
request := NewCommandRequest("", nil)

if request.Name != "" {
t.Fatalf("Name = %q, want empty", request.Name)
}
if request.Payload != nil {
t.Fatalf("Payload = %s, want nil", request.Payload)
}
if request.RequestedAt == nil {
t.Fatal("RequestedAt is nil")
}
}

func TestCommandDescriptorJSON(t *testing.T) {
descriptor := CommandDescriptor{
Name: "cache/refresh",
Expand Down
15 changes: 11 additions & 4 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,10 @@ context for admin surfaces.

`ComponentInfo` does not currently include labels. Stable operational metadata
belongs on `Attribute` fields in the relevant read model, such as status,
inspection, check descriptors, command descriptors, command requests or results,
and future event records if Opskit later defines an event envelope. Opskit may
add identity-level labels later if sibling kits demonstrate a concrete need
before those read models are available.
readiness, inspection, check descriptors or results, command descriptors,
command requests or results, and future event records if Opskit later defines
an event envelope. Opskit may add identity-level labels later if sibling kits
demonstrate a concrete need before those read models are available.

### `ComponentFunc`

Expand Down Expand Up @@ -681,12 +681,19 @@ type CommandRequest struct {
RequestedAt *time.Time `json:"requested_at,omitempty"`
Attributes []Attribute `json:"attributes,omitempty"`
}

func NewCommandRequest(name string, payload json.RawMessage, attrs ...Attribute) CommandRequest
```

`Payload` is command-specific raw JSON. Presentation layers that accept payloads
from users must perform authentication, authorization, validation, and size
limits before constructing a `CommandRequest`.

`NewCommandRequest` sets `RequestedAt` to the current UTC time and defensively
copies payload bytes and attributes. It does not validate command names or
payloads. Callers that need a custom `RequestedAt` can construct
`CommandRequest` directly.

### `CommandResult`

```go
Expand Down
6 changes: 3 additions & 3 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,9 @@ type Component interface {

`ComponentInfo` gives the component a stable operational identity. Identity is
intentionally limited to name, kind, and description for now. Stable operational
metadata should be represented with `Attribute` values on status, inspection,
check descriptors, command descriptors, command requests or results, and future
event records if Opskit later defines an event envelope.
metadata should be represented with `Attribute` values on status, readiness,
inspection, check descriptors or results, command descriptors, command requests
or results, and future event records if Opskit later defines an event envelope.

Opskit may add `ComponentInfo` labels later if sibling kits demonstrate a
concrete need for stable identity-level metadata before active status,
Expand Down
5 changes: 1 addition & 4 deletions examples/commands/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,7 @@ func main() {
log.Fatal(err)
}

request := opskit.CommandRequest{
Name: "cache/refresh",
Payload: json.RawMessage(`{"force":true}`),
}
request := opskit.NewCommandRequest("cache/refresh", json.RawMessage(`{"force":true}`))

fmt.Println("described commands")
printJSON(commands)
Expand Down
16 changes: 16 additions & 0 deletions registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,11 @@ func (r *Registry) Inspect(ctx context.Context, name string) (Inspection, error)
}

// Checker returns a registered component as a Checker.
//
// The returned Checker is a raw active execution handle. Registry.Checker only
// discovers capability support; it does not invoke Check or provide timeout,
// panic recovery, retry, authorization, auditing, concurrency control, or
// telemetry.
func (r *Registry) Checker(name string) (Checker, error) {
component, ok := r.Component(name)
if !ok {
Expand Down Expand Up @@ -445,6 +450,11 @@ func (r *Registry) Checks(ctx context.Context, name string) ([]CheckDescriptor,
}

// CheckGroup returns a registered component as a CheckGroup.
//
// The returned CheckGroup is a raw active execution handle. Registry.CheckGroup
// only discovers capability support; it does not invoke CheckAll or provide
// timeout, panic recovery, retry, authorization, auditing, concurrency control,
// or telemetry.
func (r *Registry) CheckGroup(name string) (CheckGroup, error) {
component, ok := r.Component(name)
if !ok {
Expand All @@ -460,6 +470,12 @@ func (r *Registry) CheckGroup(name string) (CheckGroup, error) {
}

// CommandHandler returns a registered component as a CommandHandler.
//
// The returned CommandHandler is a raw active execution handle.
// Registry.CommandHandler only discovers capability support; it does not invoke
// HandleCommand or provide dispatch, timeout, panic recovery, retry,
// authorization, auditing, concurrency control, payload validation, or
// telemetry.
func (r *Registry) CommandHandler(name string) (CommandHandler, error) {
component, ok := r.Component(name)
if !ok {
Expand Down