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
83 changes: 82 additions & 1 deletion base/openapi/api.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,95 @@
}
},
"type": "object"
},
"OverviewResponse": {
"additionalProperties": false,
"properties": {
"consumes": {
"items": {
"type": "string"
},
"type": "array"
},
"solution": {
"type": "string"
}
},
"type": "object"
}
},
"info": {
"description": "The codefly-base solution's backend under the authority interface, at the route the authority's edge forwards it on: the routes it serves itself, and the module calls the solution runtime relays as the viewer. A relayed call is any checked caller's, because the child the authority issues through this solution's binding for that module, and the module's own method policy, gate it.",
"title": "codefly-base solution backend",
"version": "0.0.0"
},
"paths": {},
"paths": {
"/v1/codefly-base/overview": {
"get": {
"description": "This solution's own page, to show that it is installed and reachable as the viewer\n\nAnswers the solution's id and the `as` name of every module this backend relays, in the order they are declared. Takes no parameters and reads nothing outside this backend",
"operationId": "CodeflyBase_Get_V1_Codefly-base_Overview",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/OverviewResponse"
}
},
"401": {
"description": "The request carried no Work Context the authority checks for this solution.",
"schema": {
"$ref": "#/definitions/Error"
}
},
"502": {
"description": "A call this operation makes to a module failed.",
"schema": {
"$ref": "#/definitions/Error"
}
}
},
"summary": "report what this solution is and which modules it can reach",
"tags": [
"codefly-base"
],
"x-codefly-authority-policy": {
"requires": [
{
"resource_kind": "codefly-base.page",
"actions": [
"read"
]
}
]
},
"x-codefly-operation": {
"attempt_timeout": "30s",
"audience": "codefly-base",
"backoff": "1s",
"completion": "COMPLETION_CALL",
"invoke_scopes": [
{
"actions": [
"read"
],
"resource_kind": "codefly-base.page"
}
],
"max_attempts": 3,
"recovery": "RECOVERY_RECOMPUTE",
"retryable_codes": [
"503"
],
"tool": {
"description": "report what this solution is and which modules it can reach",
"effect": "EFFECT_READ_ONLY",
"name": "CodeflyBase_Get_V1_Codefly-base_Overview"
},
"total_timeout": "90s"
}
}
}
},
"produces": [
"application/json"
],
Expand Down
66 changes: 64 additions & 2 deletions base/operations.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package main

import solution "github.com/codefly-dev/solution-runtime-go"
import (
"context"
"net/http"

solution "github.com/codefly-dev/solution-runtime-go"
)

// route is where the authority's edge routes this solution and forwards its
// requests unchanged: /v1/<id>, the solution's alias. EVERY operation carries
Expand Down Expand Up @@ -36,5 +41,62 @@ const route = "/v1/" + solutionID
// contract does not declare is a scope the authority never issues, so the
// route would refuse every call.
func operations() []solution.Operation {
return nil
return []solution.Operation{overviewOperation()}
}

// overviewOperation is the one route a scaffolded solution serves, and the
// one its page reads.
//
// It exists so a freshly scaffolded solution is RUNNABLE AND OBSERVABLE
// rather than correct-looking and inert. Everything between a host and this
// backend has to work for its answer to appear on the page: the host mounts
// the remote, the edge routes /v1/<id>, the authority checks the viewer's
// Work Context against the policy below, and the runtime dispatches. A page
// that renders this has proven that path; a page that renders a fixed string
// has proven nothing, which is why the answer is derived rather than typed.
//
// Replace it with the solution's own first route. Keep the shape: a policy,
// the three prose fields, and a typed Response.
func overviewOperation() solution.Operation {
return solution.Operation{
Path: route + "/overview",
Method: http.MethodGet,
Summary: "report what this solution is and which modules it can reach",
Callers: "This solution's own page, to show that it is installed and reachable as the viewer",
Behavior: "Answers the solution's id and the `as` name of every module this backend relays, in the order they are declared. Takes no parameters and reads nothing outside this backend",
Response: overviewResponse{},
// The viewer must hold a read on this solution's own page. The kind is
// the solution's OWN vocabulary, which module.contract.codefly.yaml
// declares under scope_ceilings and grants through the installer role
// — both written by `codefly add solution` for exactly this name
// (cli#1216). A kind the contract does not declare is a scope the
// authority never issues, so the route would refuse every call.
Policy: solution.Requires(solutionID+".page", "read"),
Handler: overview,
}
}

// overviewResponse is what the page reads to show that this solution is
// reachable, and what it can reach.
type overviewResponse struct {
// Solution is this solution's id, as the host lists it and the authority
// issues to.
Solution string `json:"solution"`
// Consumes is the `as` route name of every module this backend relays,
// which is what consumes.go declares and the runtime opened a relay for.
// Empty on a freshly scaffolded solution, which composes none yet.
Consumes []string `json:"consumes"`
}

// overview answers from the DECLARATIONS the runtime booted with, not from
// constants: the list is the one consumed() built, so a module that failed to
// declare a method is absent here too, and the page shows what is actually
// reachable.
func overview(_ context.Context, _ *solution.Gateway) (any, error) {
relayed := consumed()
names := make([]string, 0, len(relayed))
for index := range relayed {
names = append(names, relayed[index].As)
}
return overviewResponse{Solution: solutionID, Consumes: names}, nil
}
3 changes: 2 additions & 1 deletion base_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ func TestBaseConsumptionIsWhatThisAgentGenerates(t *testing.T) {
// and it must come from the same render — otherwise base/'s suite passes on
// bytes a scaffolded tree would never hold.
func TestBaseInterfaceDocumentIsTheRuntimesRender(t *testing.T) {
server := solution.New(solution.Manifest{ID: baseSolutionID, Contract: baseSolutionID})
server := solution.New(solution.Manifest{ID: baseSolutionID, Contract: baseSolutionID}).
Operations(scaffoldedOperations(baseSolutionID)...)
document, err := server.InterfaceArtifact("", solution.InterfaceInfo{
Title: baseSolutionID + " solution backend",
Version: "0.0.0",
Expand Down
3 changes: 2 additions & 1 deletion scaffold.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,8 @@ func (s *Builder) writeGeneratedConsumption(ctx context.Context) error {
// service.codefly.yaml, which `CreateResponse` has not written yet when this
// runs.
func (s *Builder) writeInterfaceArtifact(ctx context.Context, context scaffold, version string) error {
server := solution.New(solution.Manifest{ID: context.SolutionID, Contract: context.SolutionID})
server := solution.New(solution.Manifest{ID: context.SolutionID, Contract: context.SolutionID}).
Operations(scaffoldedOperations(context.SolutionID)...)
document, err := server.InterfaceArtifact("", solution.InterfaceInfo{
Title: context.SolutionID + " solution backend",
Version: version,
Expand Down
52 changes: 52 additions & 0 deletions scaffolded_operations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package main

import (
"context"
"net/http"

solution "github.com/codefly-dev/solution-runtime-go"
)

// scaffoldedOperations is what a solution this agent scaffolds SERVES, as a
// declaration this module can render without compiling the scaffolded tree.
//
// It mirrors base/operations.go, and it has to exist because the two live in
// different Go modules: base/ is the scaffolded backend's source and cannot
// be imported from here, but the interface document this agent writes at
// Create must list exactly the routes that source serves. Render it from an
// empty server and the scaffolded tree ships a document saying it serves
// nothing while its code serves a route — the authority then derives no check
// row for a route that is being served, and the scaffolded
// TestInterfaceArtifactIsCurrent fails on a tree nobody has touched.
//
// THE DRIFT IS GUARDED, which is the only reason a second declaration is
// tolerable here: TestBaseInterfaceDocumentIsTheRuntimesRender renders this
// and compares it byte for byte against base/openapi/api.swagger.json, which
// base's own `go generate` produces from base/operations.go. Change one
// without the other and that test fails.
func scaffoldedOperations(solutionID string) []solution.Operation {
route := "/v1/" + solutionID
return []solution.Operation{{
Path: route + "/overview",
Method: http.MethodGet,
Summary: "report what this solution is and which modules it can reach",
Callers: "This solution's own page, to show that it is installed and reachable as the viewer",
Behavior: "Answers the solution's id and the `as` name of every module this backend relays, in the order they are declared. Takes no parameters and reads nothing outside this backend",
Response: overviewResponse{},
Policy: solution.Requires(solutionID+".page", "read"),
Handler: func(context.Context, *solution.Gateway) (any, error) {
// Never called: this declaration exists to be RENDERED. The
// scaffolded tree's own handler is base/operations.go's.
return overviewResponse{}, nil
},
}}
}

// overviewResponse mirrors base/operations.go's, and must stay identical: the
// document's schema is derived from this type, and the comparison against
// base's committed render is what catches a field added to one and not the
// other.
type overviewResponse struct {
Solution string `json:"solution"`
Consumes []string `json:"consumes"`
}
66 changes: 64 additions & 2 deletions templates/factory/operations.go.tmpl
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package main

import solution "github.com/codefly-dev/solution-runtime-go"
import (
"context"
"net/http"

solution "github.com/codefly-dev/solution-runtime-go"
)

// route is where the authority's edge routes this solution and forwards its
// requests unchanged: /v1/<id>, the solution's alias. EVERY operation carries
Expand Down Expand Up @@ -36,5 +41,62 @@ const route = "/v1/" + solutionID
// contract does not declare is a scope the authority never issues, so the
// route would refuse every call.
func operations() []solution.Operation {
return nil
return []solution.Operation{overviewOperation()}
}

// overviewOperation is the one route a scaffolded solution serves, and the
// one its page reads.
//
// It exists so a freshly scaffolded solution is RUNNABLE AND OBSERVABLE
// rather than correct-looking and inert. Everything between a host and this
// backend has to work for its answer to appear on the page: the host mounts
// the remote, the edge routes /v1/<id>, the authority checks the viewer's
// Work Context against the policy below, and the runtime dispatches. A page
// that renders this has proven that path; a page that renders a fixed string
// has proven nothing, which is why the answer is derived rather than typed.
//
// Replace it with the solution's own first route. Keep the shape: a policy,
// the three prose fields, and a typed Response.
func overviewOperation() solution.Operation {
return solution.Operation{
Path: route + "/overview",
Method: http.MethodGet,
Summary: "report what this solution is and which modules it can reach",
Callers: "This solution's own page, to show that it is installed and reachable as the viewer",
Behavior: "Answers the solution's id and the `as` name of every module this backend relays, in the order they are declared. Takes no parameters and reads nothing outside this backend",
Response: overviewResponse{},
// The viewer must hold a read on this solution's own page. The kind is
// the solution's OWN vocabulary, which module.contract.codefly.yaml
// declares under scope_ceilings and grants through the installer role
// — both written by `codefly add solution` for exactly this name
// (cli#1216). A kind the contract does not declare is a scope the
// authority never issues, so the route would refuse every call.
Policy: solution.Requires(solutionID+".page", "read"),
Handler: overview,
}
}

// overviewResponse is what the page reads to show that this solution is
// reachable, and what it can reach.
type overviewResponse struct {
// Solution is this solution's id, as the host lists it and the authority
// issues to.
Solution string `json:"solution"`
// Consumes is the `as` route name of every module this backend relays,
// which is what consumes.go declares and the runtime opened a relay for.
// Empty on a freshly scaffolded solution, which composes none yet.
Consumes []string `json:"consumes"`
}

// overview answers from the DECLARATIONS the runtime booted with, not from
// constants: the list is the one consumed() built, so a module that failed to
// declare a method is absent here too, and the page shows what is actually
// reachable.
func overview(_ context.Context, _ *solution.Gateway) (any, error) {
relayed := consumed()
names := make([]string, 0, len(relayed))
for index := range relayed {
names = append(names, relayed[index].As)
}
return overviewResponse{Solution: solutionID, Consumes: names}, nil
}
Loading