Skip to content
Open
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
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,36 @@ is published, and an operation missing its documentation or its policy is
refused, at generation and at boot. A `Message[T]` field is described from its
descriptor, with protobuf JSON names.

Declare required top-level request keys with
`Request: solution.RequiredFields(reviewRequest{}, "recordId", "revision", "typeRevision")`.
Names are exact JSON property names, not Go field names. This emits a separate
request definition for the operation with Swagger `required`; Core projects those fields as
`optional: false`. Other operations, responses and nested uses of the same type
remain unchanged, including recursive occurrences. No names means no change.
This declares key presence only; handlers still own nonblank, non-null and
business validation. It does not install request validation middleware.

The opt-in supports a named ordinary struct or non-nil pointer to it with
direct, uniquely named exported fields. Its reachable fields may use named
structs, strings, booleans, signed integers, floats, pointers, arrays, non-byte
slices and string-keyed maps. At generation and boot it refuses unknown, blank
or repeated required names, and rejects embedding, invalid JSON names, the JSON
`string` option, JSON/text codecs (including `time.Time`), protobuf `Message`,
byte slices and interfaces anywhere in the request graph. Two distinct types
whose names produce the same definition within that graph are also refused.
The graph has its own definition namespace, so another operation or response
using the same short type name cannot change it, nor can the built-in `Error`.
Unexported and `json:"-"` fields cannot be named. `omitempty` does not imply
input optionality. Use the wrapper only in `Request`; nested requirements are
outside this API.

The generated contract retains the generator's **non-null input subset**:
pointer and collection properties may be absent when optional, but a present
value must have the declared JSON type. Go can encode nil pointers and nil
collections as `null`; that value is outside this contract even if a handler
accepts it. This API neither adds nullability nor claims to describe every
input Go's decoder can accept.

The artifact's security scheme is the `x-codefly-work-context` header the edge
sends. Every operation also declares its `Policy`, what a call to the route must
hold: `solution.Requires(kind, actions...)` or `solution.AnyCheckedCaller()`,
Expand Down
6 changes: 6 additions & 0 deletions internal/requestschematest/first/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Package first supplies a distinct Go identity with the same short name as second.
package first

type Shared struct {
First string `json:"first"`
}
6 changes: 6 additions & 0 deletions internal/requestschematest/second/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Package second supplies a distinct Go identity with the same short name as first.
package second

type Shared struct {
Second string `json:"second"`
}
38 changes: 36 additions & 2 deletions operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ type Operation struct {
// Query lists the documented query parameters.
Query []QueryParameter
// Request is the zero value of the JSON body the operation reads, if any.
// RequiredFields wraps it to declare top-level JSON key presence.
Request any
// Response is the zero value of the body the operation returns on 200;
// the artifact's schema is derived from its type. A generated protobuf
Expand Down Expand Up @@ -410,8 +411,22 @@ func swaggerDocument(info InterfaceInfo, ops []Operation, audience string) (map[
endpoint[OperationExtension] = marker
}
parameters := []any{}
if op.Request != nil {
parameters = append(parameters, map[string]any{"name": "body", "in": "body", "required": true, "schema": valueSchema(op.Request, definitions)})
if requestValue(op.Request) != nil {
schema, err := requestSchema(op.Request, definitions, requiredRequestDefinition(op))
if err != nil {
return nil, fmt.Errorf("operation %s %s: %w", op.verb(), op.Path, err)
}
if d, wrapped := op.Request.(RequiredRequest); wrapped && len(d.names) > 0 {
// Core records the identity of a published payload definition;
// an inline body has none. Keep this operation's copy separate.
name := requiredRequestDefinition(op)
if _, exists := definitions[name]; exists {
return nil, fmt.Errorf("operation %s %s: request definition %q already exists", op.verb(), op.Path, name)
}
definitions[name] = schema
schema = map[string]any{"$ref": "#/definitions/" + name}
}
parameters = append(parameters, map[string]any{"name": "body", "in": "body", "required": true, "schema": schema})
}
for _, query := range op.Query {
parameters = append(parameters, map[string]any{"name": query.Name, "in": "query", "required": query.Required, "type": "string"})
Expand Down Expand Up @@ -531,6 +546,25 @@ func (o Operation) validate() error {
if err := o.Policy.validate(); err != nil {
return fmt.Errorf("operation %s %s %w", o.verb(), o.Path, err)
}
if _, wrapped := o.Response.(RequiredRequest); wrapped {
return fmt.Errorf("operation %s %s: RequiredFields belongs in Request, not Response", o.verb(), o.Path)
}
if _, pointer := o.Request.(*RequiredRequest); pointer {
return fmt.Errorf("operation %s %s: use the RequiredFields value directly, not a *RequiredRequest", o.verb(), o.Path)
}
if _, pointer := o.Response.(*RequiredRequest); pointer {
return fmt.Errorf("operation %s %s: RequiredFields belongs in Request, not Response", o.verb(), o.Path)
}
if d, wrapped := o.Request.(RequiredRequest); wrapped {
if _, nested := d.request.(RequiredRequest); nested {
return fmt.Errorf("operation %s %s: RequiredFields cannot wrap another RequiredRequest", o.verb(), o.Path)
}
if len(d.names) > 0 {
if _, err := requestSchema(o.Request, map[string]any{}, requiredRequestDefinition(o)); err != nil {
return fmt.Errorf("operation %s %s: %w", o.verb(), o.Path, err)
}
}
}
return nil
}

Expand Down
20 changes: 20 additions & 0 deletions qualification/installreceiver/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Installation receiver process fixture

This test-only consumer lets Accounts' `TestCanonicalCombinedInstallQueueThroughRuntime` exercise the real runtime listener and Authority Check transport. It does not implement an installation verifier. Its `OnInstall` callback appends the verified synthetic installation to the specified JSONL file, then acknowledges, refuses, or fails as directed by the test.

When `INSTALL_RECEIVER_SCHEMA` names a protobuf-JSON RecordType, acknowledgement also requires two `DefineRecordType` calls through a declared setup binding, with the second confirming the same identity and revision without minting another revision. An undeclared `DefineProjection` call must be refused. The composing test must provide the `records/ENDPOINT` and `records/BINDING` workspace configuration slots and deliver matching setup, method and owner-aware policies. This mode is not evidence until run through a real composed Authority, hop and Records service.

The Accounts test owns all process configuration, including the solution ID, log path, outcome, Authority endpoint, workload token file and native listener. Run it only with disposable owner test dependencies.

Build with a Go workspace selecting the desired compatible runtime, Core, Authority client and hop revisions, then pass the resulting executable to the Accounts test:

```sh
cd qualification/installreceiver
go build -o /absolute/path/installreceiver .
shasum -a 256 /absolute/path/installreceiver
# From module-saas-starter/module/services/accounts/code, with the same cohort:
INSTALL_RECEIVER_BINARY=/absolute/path/installreceiver go test -count=1 -v -run '^TestCanonicalCombinedInstallQueueThroughRuntime$' ./pkg/adapters
```

Record the source and dependency revisions, workspace selection, binary hash, Codefly version, and test output alongside the run. A skipped test is not a crossing proof.
The nested module uses the published Records client; qualifying unreleased owner changes requires an explicit local Go workspace selecting the matching client, runtime, Core and Authority libraries. Do not commit local replacement paths.
9 changes: 9 additions & 0 deletions qualification/installreceiver/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module github.com/codefly-dev/solution-runtime-go/qualification/installreceiver

go 1.27.3

require (
github.com/codefly-dev/solution-runtime-go v0.0.17-0.20261031211909-86f9d9eb3da8
github.com/obin-ai/module-record-store/clients/go v0.4.4
google.golang.org/protobuf v1.36.12
)
2 changes: 2 additions & 0 deletions qualification/installreceiver/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/obin-ai/module-record-store/clients/go v0.4.4 h1:vPBygkiUx1uvfXoWkg0D8BMXEabih+ZgJd7PcHNfwWI=
github.com/obin-ai/module-record-store/clients/go v0.4.4/go.mod h1:EozaQ6pnMueL1IvuvH/tbcn7apw0YQqnsPmpKX0nR9k=
84 changes: 84 additions & 0 deletions qualification/installreceiver/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// installreceiver is a process fixture for the Accounts installation crossing.
package main

import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"os"

solution "github.com/codefly-dev/solution-runtime-go"
recordsv1 "github.com/obin-ai/module-record-store/clients/go/records/v1"
"google.golang.org/protobuf/encoding/protojson"
)

func main() {
id := os.Getenv("SOLUTION_ID")
path := os.Getenv("INSTALL_RECEIVER_LOG")
outcome := os.Getenv("INSTALL_RECEIVER_OUTCOME")
schemaPath := os.Getenv("INSTALL_RECEIVER_SCHEMA")
if id == "" || path == "" || (outcome != "ack" && outcome != "refuse" && outcome != "fail") {
log.Fatal("SOLUTION_ID, INSTALL_RECEIVER_LOG and a valid INSTALL_RECEIVER_OUTCOME are required")
}
var schema recordsv1.RecordType
if schemaPath != "" {
data, err := os.ReadFile(schemaPath)
if err != nil {
log.Fatal(err)
}
if err := protojson.Unmarshal(data, &schema); err != nil {
log.Fatal(err)
}
}
step := func(ctx context.Context, installation solution.Installation) error {
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
if err != nil {
return fmt.Errorf("open observation log: %w", err)
}
err = json.NewEncoder(file).Encode(installation)
closeErr := file.Close()
if err != nil {
return fmt.Errorf("write observation: %w", err)
}
if closeErr != nil {
return fmt.Errorf("close observation log: %w", closeErr)
}
switch outcome {
case "refuse":
return solution.RefuseInstall("fixture refused installation")
case "fail":
return errors.New("fixture installation failed")
default:
if schemaPath == "" {
return nil
}
request := &recordsv1.DefineRecordTypeRequest{Name: schema.GetName(), Type: &schema}
first := &recordsv1.DefineRecordTypeResponse{}
if err := installation.Call(ctx, "records", recordsv1.RecordsService_DefineRecordType_FullMethodName, request, first); err != nil {
return fmt.Errorf("define schema: %w", err)
}
again := &recordsv1.DefineRecordTypeResponse{}
if err := installation.Call(ctx, "records", recordsv1.RecordsService_DefineRecordType_FullMethodName, request, again); err != nil {
return fmt.Errorf("repeat schema: %w", err)
}
if first.GetType().GetTypeId() == "" || first.GetType().GetTypeRevision() == "" || again.GetMinted() || first.GetType().GetTypeId() != again.GetType().GetTypeId() || first.GetType().GetTypeRevision() != again.GetType().GetTypeRevision() {
return errors.New("schema definition did not converge")
}
if err := installation.Call(ctx, "records", recordsv1.RecordsService_DefineProjection_FullMethodName, &recordsv1.DefineProjectionRequest{}, &recordsv1.DefineProjectionResponse{}); err == nil {
return errors.New("undeclared method was accepted")
}
return nil
}
}
server := solution.New(solution.Manifest{ID: id})
if schemaPath == "" {
server.OnInstall(step)
} else {
server.OnInstall(step, solution.SetupModule{As: "records", Authority: solution.AuthorityRelay{Group: "records", EndpointKey: "ENDPOINT", BindingKey: "BINDING"}, Methods: []string{recordsv1.RecordsService_DefineRecordType_FullMethodName}})
}
if err := server.Serve(); err != nil {
log.Fatal(err)
}
}
Loading