Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7a9cf68
refactor(client): keep the URLs of the API docs in client/openapi
grubmeshi Oct 16, 2026
6d43c0b
feat(client): read the schema model and referenced responses of the A…
grubmeshi Oct 16, 2026
8a91f23
feat(client): read a whole listing with pages.ReadAll
grubmeshi Oct 16, 2026
a0d239a
feat(client): read and expand the HAL links of a meshObject
grubmeshi Oct 16, 2026
fee1f31
feat(client): send the requests of a generated client
grubmeshi Oct 16, 2026
e79bc83
feat(client): generate the types and a typed client of meshObject kinds
grubmeshi Oct 16, 2026
22a97bd
feat(client): generate meshWorkspace v2, meshBuildingBlock v2 and mes…
grubmeshi Oct 16, 2026
3688f72
feat(client): add the generated clients to client.Client and the raw …
grubmeshi Oct 16, 2026
312a2a0
refactor: read workspaces, building blocks and runs through the gener…
grubmeshi Oct 16, 2026
2f82f2e
feat(client): read deprecations and path keys from the API docs
grubmeshi Oct 22, 2026
221d956
refactor(client): send the requests of generated clients through clie…
grubmeshi Oct 22, 2026
24e95ad
feat(client): mark deprecated API versions and name path parameters a…
grubmeshi Oct 22, 2026
5cd78c0
feat(client): mark a deprecated version as removed after its end of l…
grubmeshi Oct 23, 2026
99d37f3
feat(client): type the query parameters of generated clients by their…
grubmeshi Oct 23, 2026
71c8b7c
feat(client): take typed filters in the generated clients
grubmeshi Oct 23, 2026
ae3276c
refactor(client): regenerate meshBuildingBlock v2 and meshBuildingBlo…
grubmeshi Oct 25, 2026
546de24
feat(client): mark deprecated fields and filters of generated clients
grubmeshi Oct 26, 2026
c7746f2
refactor(client): regenerate meshBuildingBlock v2 from the final spec
grubmeshi Oct 26, 2026
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
5 changes: 5 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ linters:
linters:
- forbidigo
text: one HTTP client
# The generator downloads the API docs in a go:generate run, where no front end exists.
- path: client/openapi/generator/main\.go
linters:
- forbidigo
text: user agent of the front end
# The fake clock is a test's; production code waits on the real one.
- path-except: _test\.go
linters:
Expand Down
26 changes: 23 additions & 3 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package client
import (
"context"

"github.com/meshcloud/meshstack-cli/client/internal"
"github.com/meshcloud/meshstack-cli/client/meshapi"
"github.com/meshcloud/meshstack-cli/client/meshbuildingblock/v2"
"github.com/meshcloud/meshstack-cli/client/meshbuildingblockrun/v1"
"github.com/meshcloud/meshstack-cli/client/meshworkspace/v2"
"github.com/meshcloud/meshstack-cli/client/types/xurl"
"github.com/meshcloud/meshstack-cli/internal/http"
"github.com/meshcloud/meshstack-cli/internal/version"
Expand Down Expand Up @@ -46,12 +49,19 @@ type Client struct {
WorkspaceUserBinding MeshWorkspaceUserBindingClient
Raw *RawClient

// The generated clients, one per kind in one API version.
MeshWorkspace meshworkspace.Client
MeshBuildingBlock meshbuildingblock.Client
MeshBuildingBlockRun meshbuildingblockrun.Client
// HttpClient is what the NewClient of a package that another module generates takes.
HttpClient meshapi.HttpClient

// Endpoint is read by the Terraform provider's meshstack_instance data source.
Endpoint xurl.URL
}

func New(ctx context.Context, endpoint xurl.URL, client http.Client, auth Authorization) Client {
authorizedClient := internal.HttpClient{
authorizedClient := meshapi.HttpClient{
AuthorizedClient: client.WithAuthorization(auth),
EndpointUrl: endpoint,
}
Expand All @@ -60,6 +70,9 @@ func New(ctx context.Context, endpoint xurl.URL, client http.Client, auth Author
buildingBlockDefinitionVersion := newBuildingBlockDefinitionVersionClient(ctx, authorizedClient)
buildingBlockRun := newBuildingBlockRunClient(ctx, authorizedClient)
workspace := newWorkspaceClient(ctx, authorizedClient)
generatedWorkspace := meshworkspace.NewClient(authorizedClient)
generatedBuildingBlock := meshbuildingblock.NewClient(authorizedClient)
generatedBuildingBlockRun := meshbuildingblockrun.NewClient(authorizedClient)
return Client{
ApiKey: newApiKeyClient(ctx, authorizedClient),
BuildingBlock: newBuildingBlockClient(ctx, authorizedClient),
Expand Down Expand Up @@ -91,7 +104,14 @@ func New(ctx context.Context, endpoint xurl.URL, client http.Client, auth Author
with(buildingBlockRun.meshObject).
with(buildingBlockDefinition.meshObject).
withOwnOrder(buildingBlockDefinitionVersion.meshObject).
with(newEventLogMeshObject(ctx, authorizedClient)),
with(newEventLogMeshObject(ctx, authorizedClient)).
withApi[meshworkspace.Response](generatedWorkspace.Api()).
withApi[meshbuildingblock.Response](generatedBuildingBlock.Api()).
withApi[meshbuildingblockrun.Response](generatedBuildingBlockRun.Api()),
MeshWorkspace: generatedWorkspace,
MeshBuildingBlock: generatedBuildingBlock,
MeshBuildingBlockRun: generatedBuildingBlockRun,
HttpClient: authorizedClient,

Endpoint: endpoint,
}
Expand Down
3 changes: 3 additions & 0 deletions client/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package client

//go:generate go run ./internal/generate
33 changes: 33 additions & 0 deletions client/internal/generate/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Command generate writes the packages of the meshObject kinds below client/.
package main

import (
"github.com/meshcloud/meshstack-cli/client/openapi"
"github.com/meshcloud/meshstack-cli/client/openapi/generator"
)

func main() {
generator.Main(generator.Config{
Module: "github.com/meshcloud/meshstack-cli/client",
Shared: "meshobject",
Packages: []generator.Package{
{Kind: "meshWorkspace", ApiVersion: "v2", Actions: []string{"list", "show", "create", "update", "delete"}},
{Kind: "meshBuildingBlock", ApiVersion: "v2-preview", Actions: []string{"list", "show", "create", "update", "delete", "purge", "trigger-run"}},
{Kind: "meshBuildingBlockRun", ApiVersion: "v1", Actions: []string{"list", "show", "logs"}},
},
MapType: mapType,
})
}

const typesImport = "github.com/meshcloud/meshstack-cli/client/types"

func mapType(at generator.Location, _ *openapi.Schema) (generator.GoType, bool) {
switch at {
case generator.Location{Schema: "MeshBuildingBlockInput", Property: "value"}:
return generator.GoType{Expr: "types.SecretOrAny", Import: typesImport, Container: true}, true
case generator.Location{Schema: "MeshBuildingBlockOutput", Property: "value"},
generator.Location{Schema: "MeshBuildingBlockRun.MeshBuildingBlockRunSpec.MeshBuildingBlockInputsForRun", Property: "value"}:
return generator.GoType{Expr: "any", Container: true}, true
}
return generator.GoType{}, false
}
8 changes: 2 additions & 6 deletions client/internal/mesh_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,11 @@ import (
"strings"
"unicode"

"github.com/meshcloud/meshstack-cli/client/types/xurl"
"github.com/meshcloud/meshstack-cli/client/meshapi"
"github.com/meshcloud/meshstack-cli/internal/http"
)

type HttpClient struct {
http.AuthorizedClient

EndpointUrl xurl.URL
}
type HttpClient = meshapi.HttpClient

// MeshObjectApi is the API of one meshObject kind, in one version, with no Go type for its objects:
// the requests and the paging they share, for a caller that decodes the answers itself.
Expand Down
173 changes: 173 additions & 0 deletions client/meshapi/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// Package meshapi sends the requests of the clients that client/openapi/generator writes. It is
// public so that another module can generate clients of its own.
package meshapi

import (
"context"
"encoding/json/jsontext"
"encoding/json/v2"
"errors"
"fmt"
"iter"
"net/url"
"reflect"
"strings"

"github.com/meshcloud/meshstack-cli/client/openapi"
"github.com/meshcloud/meshstack-cli/client/types/xurl"
"github.com/meshcloud/meshstack-cli/internal/http"
internaljson "github.com/meshcloud/meshstack-cli/internal/json"
)

// HttpClient sends requests to one meshStack, authorized.
type HttpClient struct {
http.AuthorizedClient

EndpointUrl xurl.URL
}

// Api is one version of a meshObject kind as the generated clients call it.
type Api struct {
HttpClient

Kind string
ApiVersion string
MediaType string
// Path is that of the kind's collection, such as /api/meshobjects/meshworkspaces.
Path string
// EmbeddedRel names the objects in a page of the listing.
EmbeddedRel string
}

// Call is one request of a generated client.
type Call struct {
Method string
Path string
Query any
// SendsMeshObject and AnswersMeshObject mark a body and an answer that carry the kind and
// apiVersion, which the generated types leave out.
SendsMeshObject bool
AnswersMeshObject bool
}

// Path fills the parameters of the template in their order.
func Path(template string, values ...string) string {
var path strings.Builder
rest := template
for _, value := range values {
before, after, ok := strings.Cut(rest, "{")
_, after, closed := strings.Cut(after, "}")
if !ok || !closed {
panic(fmt.Sprintf("%s has fewer parameters than %d", template, len(values)))
}
path.WriteString(before + url.PathEscape(value))
rest = after
}
return path.String() + rest
}

// Do answers a zero R for an empty answer, which only an R of type any expects.
func (a Api) Do[R any](ctx context.Context, call Call, body any, options ...http.RequestOption) (R, error) {
var result R
options = append(options, http.WithAccept(a.MediaType))
if call.Query != nil {
options = append(options, http.WithUrlQuery(call.Query))
}
if call.Method == http.MethodPut || call.Method == http.MethodDelete {
options = append(options, http.Retryable())
}
if body != nil {
payload, err := a.payload(body, call.SendsMeshObject)
if err != nil {
return result, err
}
options = append(options, http.WithJsonPayload(payload, a.MediaType))
}
answer, err := a.DoRequest[[]byte](ctx, call.Method, a.EndpointUrl.JoinPath(call.Path), options...)
if err != nil {
return result, err
}
if len(answer) == 0 {
if t := reflect.TypeFor[R](); t.Kind() == reflect.Interface && t.NumMethod() == 0 {
return result, nil
}
return result, fmt.Errorf("unexpected empty response body from %s %s", call.Method, call.Path)
}
return result, a.decode(answer, call.AnswersMeshObject, &result)
}

type page struct {
Embedded map[string][]jsontext.Value `json:"_embedded"`
Page struct {
TotalElements int `json:"totalElements"`
TotalPages int `json:"totalPages"`
Number int `json:"number"`
} `json:"page"`
}

// List pages through the objects that call lists.
func (a Api) List[T any](ctx context.Context, call Call) iter.Seq2[T, error] {

Check failure on line 109 in client/meshapi/api.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 20 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=meshcloud_meshstack-cli&issues=AaFj4fFmZGnBBwyg-Qg0&open=AaFj4fFmZGnBBwyg-Qg0&pullRequest=50
return func(yield func(T, error) bool) {
var noItem T
for pageNumber := 0; ; pageNumber++ {
response, err := a.Do[page](ctx, Call{Method: call.Method, Path: call.Path, Query: call.Query}, nil,
http.WithUrlQuery(map[string]any{"page": pageNumber}))
if err != nil && pageNumber > 0 {
err = fmt.Errorf("cannot read page %d of the list: %w", pageNumber, err)
}
if err != nil {
yield(noItem, err)
return
}
items, ok := response.Embedded[a.EmbeddedRel]
if !ok && response.Page.TotalElements > 0 {
yield(noItem, fmt.Errorf("embedded key %s not found in paginated response", a.EmbeddedRel))
return
}
for _, raw := range items {
var item T
if err := a.decode(raw, call.AnswersMeshObject, &item); err != nil {
yield(noItem, err)
return
}
if !yield(item, nil) {
return
}
}
if response.Page.Number >= response.Page.TotalPages-1 {
return
}
}
}
}

func (a Api) payload(body any, meshObject bool) (jsontext.Value, error) {
encoded, err := internaljson.Marshal(body)
if err != nil || !meshObject {
return encoded, err
}
version, err := openapi.ParseApiVersion(a.ApiVersion)
if err != nil {
return nil, err
}
return openapi.MediaType{Name: a.MediaType, Kind: a.Kind, ApiVersion: version}.WithKindAndApiVersion(encoded)
}

func (a Api) decode(answer []byte, meshObject bool, target any) error {
if meshObject {
var header struct {
Kind string `json:"kind"`
ApiVersion string `json:"apiVersion"`
}
if err := json.Unmarshal(answer, &header); err != nil {
return fmt.Errorf("parsing response body as JSON failed: %w", err)
}
if header.Kind != a.Kind || header.ApiVersion != a.ApiVersion {
return errors.New("meshStack answered " + header.Kind + " " + header.ApiVersion + " for " + a.Kind + " " + a.ApiVersion)
}
}
if err := json.Unmarshal(answer, target); err != nil {
return fmt.Errorf("parsing response body as JSON failed: %w", err)
}
return nil
}
Loading
Loading