Skip to content

Commit 9511c44

Browse files
authored
fix(codegen): generate service accessors (#152)
Sales and Tokens have generated service files but cannot be accessed through the PHP client. Service accessors were maintained by hand, and Composer listed generated files individually. Generate a service-accessor trait from the same API groups as the services and use a directory classmap for autoloading. The hand-written client keeps its authentication and event methods. New API groups no longer need manual registration. Add a new-group generator regression, exercise Sales and Tokens through the configured client, and check in CI that regeneration leaves no missing or stale SDK files.
1 parent 5b100d6 commit 9511c44

10 files changed

Lines changed: 275 additions & 361 deletions

File tree

‎.github/workflows/codegen.yaml‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ on:
66
- main
77
paths:
88
- "codegen/**"
9+
- "src/**"
910
- "openapi.json"
1011
- "composer.json"
1112
- "justfile"
@@ -15,6 +16,7 @@ on:
1516
- main
1617
paths:
1718
- "codegen/**"
19+
- "src/**"
1820
- "openapi.json"
1921
- "composer.json"
2022
- "justfile"
@@ -65,3 +67,37 @@ jobs:
6567

6668
- name: Run tests
6769
run: go test ./...
70+
71+
generated:
72+
name: Generated SDK
73+
runs-on: ubuntu-latest
74+
steps:
75+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
76+
77+
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
78+
with:
79+
go-version-file: './codegen/go.mod'
80+
81+
- name: Set up PHP
82+
uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # v2.40.0
83+
with:
84+
php-version: '8.2.35'
85+
coverage: none
86+
tools: composer:v2
87+
88+
- name: Install dependencies
89+
run: composer install --prefer-dist --no-progress --no-interaction
90+
working-directory: .
91+
92+
- name: Generate SDK
93+
run: go run . generate --out ../src ../openapi.json
94+
95+
- name: Format generated SDK
96+
run: vendor/bin/php-cs-fixer fix --using-cache=no
97+
working-directory: .
98+
99+
- name: Ensure generated SDK is up to date
100+
run: |
101+
git diff --exit-code -- src
102+
test -z "$(git ls-files --others --exclude-standard -- src)"
103+
working-directory: .

‎codegen/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,4 @@ class CheckoutRequest { /* ... */ }
8181

8282
#### Autoloading
8383

84-
Generated files with multiple classes and enums are added to the `classmap` in `composer.json` to ensure proper autoloading.
84+
Composer's `classmap` covers `src/`, including generated files with multiple classes and enums. The generator also writes `src/ServiceAccessors.php`, which provides the API group methods used by `SumUp`. Adding an API group to the specification therefore adds its client accessor and autoloading without a manual registration step.

‎codegen/pkg/generator/generator.go‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,9 @@ func (g *Generator) Build() error {
149149

150150
slog.Info("models generated", slog.Int("tags", len(tagKeys)))
151151

152-
// Skip SumUp class generation to avoid breaking existing code
153-
// TODO: Fix Authorization service dependency before re-enabling
154-
// if err := g.writeSumUpClass(); err != nil {
155-
// return err
156-
// }
152+
if err := g.writeServiceAccessors(); err != nil {
153+
return err
154+
}
157155

158156
if err := g.writeApiVersion(); err != nil {
159157
return err
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package generator
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"maps"
7+
"os"
8+
"path/filepath"
9+
"slices"
10+
11+
"github.com/iancoleman/strcase"
12+
)
13+
14+
// writeServiceAccessors keeps the generated API groups separate from the hand-written client.
15+
func (g *Generator) writeServiceAccessors() error {
16+
var services []string
17+
for _, tagKey := range slices.Sorted(maps.Keys(g.operationsByTag)) {
18+
if g.shouldIncludeService(tagKey, g.operationsByTag[tagKey]) {
19+
services = append(services, g.displayTagName(tagKey))
20+
}
21+
}
22+
23+
var buf bytes.Buffer
24+
buf.WriteString("<?php\n\n// Code generated by sumup-php/codegen. DO NOT EDIT.\nnamespace SumUp;\n\n")
25+
for _, service := range services {
26+
fmt.Fprintf(&buf, "use SumUp\\Services\\%s;\n", service)
27+
}
28+
buf.WriteString("\n/** API groups available through the SumUp client. */\ntrait ServiceAccessors\n{\n")
29+
for i, service := range services {
30+
if i > 0 {
31+
buf.WriteString("\n")
32+
}
33+
buf.WriteString(" /**\n")
34+
fmt.Fprintf(&buf, " * Access the %s API endpoints.\n", service)
35+
buf.WriteString(" */\n")
36+
fmt.Fprintf(&buf, " public function %s(): %s\n", strcase.ToLowerCamel(service), service)
37+
buf.WriteString(" {\n")
38+
fmt.Fprintf(&buf, " return new %s($this->client, $this->resolveAccessToken());\n", service)
39+
buf.WriteString(" }\n")
40+
}
41+
buf.WriteString("}\n")
42+
43+
filename := filepath.Join(g.cfg.Out, "ServiceAccessors.php")
44+
if err := os.WriteFile(filename, buf.Bytes(), 0o644); err != nil {
45+
return fmt.Errorf("write service accessors: %w", err)
46+
}
47+
return nil
48+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package generator
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"strings"
7+
"testing"
8+
9+
"github.com/pb33f/libopenapi"
10+
)
11+
12+
func TestGenerateServiceAccessors(t *testing.T) {
13+
t.Parallel()
14+
const spec = `{
15+
"openapi": "3.1.0",
16+
"info": {"title": "Widgets", "version": "1.0.0"},
17+
"paths": {"/widgets": {"get": {
18+
"tags": ["Widgets"], "operationId": "listWidgets",
19+
"responses": {"204": {"description": "No widgets"}}
20+
}}}
21+
}`
22+
doc, err := libopenapi.NewDocument([]byte(spec))
23+
if err != nil {
24+
t.Fatal(err)
25+
}
26+
model, err := doc.BuildV3Model()
27+
if err != nil {
28+
t.Fatal(err)
29+
}
30+
out := t.TempDir()
31+
g := New(Config{Out: out})
32+
if err := g.Load(&model.Model); err != nil {
33+
t.Fatal(err)
34+
}
35+
if err := g.Build(); err != nil {
36+
t.Fatal(err)
37+
}
38+
data, err := os.ReadFile(filepath.Join(out, "ServiceAccessors.php"))
39+
if err != nil {
40+
t.Fatal(err)
41+
}
42+
for _, snippet := range []string{
43+
`use SumUp\Services\Widgets;`,
44+
`public function widgets(): Widgets`,
45+
`return new Widgets($this->client, $this->resolveAccessToken());`,
46+
} {
47+
if !strings.Contains(string(data), snippet) {
48+
t.Errorf("service accessors missing %q", snippet)
49+
}
50+
}
51+
}

0 commit comments

Comments
 (0)