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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@
- name: Format SDK code
run: ./gradlew spotlessApply

- name: Compile generated code samples
run: ./gradlew :sumup-sdk:compileCodeSamples

- name: Ensure clean working tree
run: git diff --exit-code

Expand Down Expand Up @@ -81,7 +84,7 @@
cache: gradle

- name: Configure toolchain paths
run: |

Check warning on line 87 in .github/workflows/ci.yaml

View workflow job for this annotation

GitHub Actions / Lint

[actionlint] reported by reviewdog 🐶 shellcheck reported issue in this script: SC2086:info:2:133: Double quote to prevent globbing and word splitting [shellcheck] Raw Output: i:.github/workflows/ci.yaml:87:9: shellcheck reported issue in this script: SC2086:info:2:133: Double quote to prevent globbing and word splitting [shellcheck]

Check warning on line 87 in .github/workflows/ci.yaml

View workflow job for this annotation

GitHub Actions / Lint

[actionlint] reported by reviewdog 🐶 shellcheck reported issue in this script: SC2086:info:1:68: Double quote to prevent globbing and word splitting [shellcheck] Raw Output: i:.github/workflows/ci.yaml:87:9: shellcheck reported issue in this script: SC2086:info:1:68: Double quote to prevent globbing and word splitting [shellcheck]
echo "JAVA_TEST_HOME=${{ steps.setup-test-java.outputs.path }}" >> $GITHUB_ENV
echo "ORG_GRADLE_JAVA_INSTALLATIONS_PATHS=${{ steps.setup-test-java.outputs.path }}:${{ steps.setup-gradle-java.outputs.path }}" >> $GITHUB_ENV

Expand Down
123 changes: 123 additions & 0 deletions .github/workflows/release-code-samples.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
name: Release Code Samples

on:
release:
types:
- published

concurrency:
group: release-code-samples-${{ github.event.release.tag_name }}
cancel-in-progress: true

permissions:
contents: read

jobs:
sync-java-code-samples:
name: Sync Java code samples
runs-on: ubuntu-latest
env:
TARGET_REPOSITORY: sumup/sumup-developer
TARGET_BRANCH: automation/java-code-samples
TARGET_FILE: src/codesamples/java.json
steps:
- name: Checkout source code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: refs/tags/${{ github.event.release.tag_name }}
persist-credentials: false

- name: Install Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: codegen/go.mod

- name: Create GitHub App token
id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
app-id: ${{ secrets.SUMUP_BOT_APP_ID }}
private-key: ${{ secrets.SUMUP_BOT_PRIVATE_KEY }}
owner: sumup
repositories: sumup-developer

- name: Checkout target repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: ${{ env.TARGET_REPOSITORY }}
ref: main
token: ${{ steps.app-token.outputs.token }}
path: sumup-developer
persist-credentials: true

- name: Get GitHub App User ID
id: get-user-id
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"

- name: Configure git
run: |
git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]'
git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com'

- name: Prepare target branch
working-directory: sumup-developer
run: |
git fetch origin "${{ env.TARGET_BRANCH }}:refs/remotes/origin/${{ env.TARGET_BRANCH }}" || true
git checkout -B "${{ env.TARGET_BRANCH }}" origin/main

- name: Generate Java code samples
run: |
mkdir -p "sumup-developer/$(dirname "${{ env.TARGET_FILE }}")"
go -C codegen run . samples \
--spec ../openapi.json \
--sdk-version-file ../VERSION \
--out "../sumup-developer/${{ env.TARGET_FILE }}"

- name: Commit generated samples
id: commit
working-directory: sumup-developer
run: |
git add "${{ env.TARGET_FILE }}"
if git diff --cached --quiet; then
echo "changed=false" >> "$GITHUB_OUTPUT"
exit 0
fi

git commit -m "chore: update Java code samples for ${{ github.event.release.tag_name }}"
echo "changed=true" >> "$GITHUB_OUTPUT"

- name: Push branch
if: steps.commit.outputs.changed == 'true'
working-directory: sumup-developer
run: git push --force-with-lease origin "${{ env.TARGET_BRANCH }}"

- name: Create or update pull request
if: steps.commit.outputs.changed == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
head_ref="sumup:${{ env.TARGET_BRANCH }}"
pr_url="$(gh pr list \
--repo "${{ env.TARGET_REPOSITORY }}" \
--head "$head_ref" \
--base main \
--state open \
--json url \
--jq '.[0].url')"

if [ -n "$pr_url" ]; then
gh pr edit "$pr_url" \
--repo "${{ env.TARGET_REPOSITORY }}" \
--title "chore: update Java code samples" \
--body "Updates \`${{ env.TARGET_FILE }}\` from \`${{ github.repository }}\` release \`${{ github.event.release.tag_name }}\`."
exit 0
fi

gh pr create \
--repo "${{ env.TARGET_REPOSITORY }}" \
--base main \
--head "${{ env.TARGET_BRANCH }}" \
--title "chore: update Java code samples" \
--body "Updates \`${{ env.TARGET_FILE }}\` from \`${{ github.repository }}\` release \`${{ github.event.release.tag_name }}\`."
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ build/
.classpath
.project
.settings

# Generated developer portal artifact
/code-samples.json
18 changes: 16 additions & 2 deletions codegen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@

Custom Go-based generator that reads the repository’s `openapi.json` and emits the Java SDK in a tag-based structure. Each tag becomes its own client and the runtime is kept intentionally small so we can iterate quickly.

## Quickstart
## Java SDK

The `generate` command reads `openapi.json` and generates the Java client, grouped API clients, models, and supporting source files. Generate the SDK from the repository root with:

```bash
go -C codegen run . generate
just generate
```

### CLI flags
Expand All @@ -19,3 +21,15 @@ go -C codegen run . generate
- `--package` (default `com.sumup.sdk`) – base package for generated classes.

The command is idempotent; rerunning it rewrites the generated clients in-place. Continuous Integration runs the same invocation and fails when the working tree is dirty afterward.

## Java Code Samples

The `samples` command generates a deterministic, versioned JSON catalog of Java examples from the same intermediate representation used to generate the SDK. Each catalog entry contains a complete Java program. Named OpenAPI request examples produce separate entries.

Generate a catalog from the repository root with:

```bash
just generate-codesamples
```

The recipe writes `code-samples.json` in the repository root by default. Pass another path as its argument to use a different destination. Every generated program is compiled in Continuous Integration. When an SDK release is published, the release workflow regenerates the catalog from that tag and opens or updates a pull request in `sumup/sumup-developer`; the generated JSON is not committed to this repository.
1 change: 1 addition & 0 deletions codegen/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func App() *cli.App {
DefaultCommand: "generate",
Commands: []*cli.Command{
GenerateCommand(),
SamplesCommand(),
},
}
}
2 changes: 1 addition & 1 deletion codegen/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/lmittmann/tint v1.2.0
github.com/pb33f/libopenapi v0.38.7
github.com/urfave/cli/v2 v2.27.7
go.yaml.in/yaml/v4 v4.0.0-rc.6
)

require (
Expand All @@ -16,6 +17,5 @@ require (
github.com/pb33f/ordered-map/v2 v2.3.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect
golang.org/x/sync v0.22.0 // indirect
)
14 changes: 13 additions & 1 deletion codegen/internal/generator/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ type operationModel struct {
HasOptionalHeaders bool
HasOptionalArgs bool
TagName string
Operation *v3.Operation
RequestSchema *base.SchemaProxy
}

// parameterGroupModel holds information about optional parameter structs
Expand All @@ -78,6 +80,8 @@ type parameterModel struct {
Required bool
Type javaType
Location string
Schema *base.SchemaProxy
Parameter *v3.Parameter
}

// schemaModel represents the information required to render a POJO model.
Expand All @@ -97,11 +101,13 @@ type schemaModel struct {

// schemaField stores metadata for a field within a schemaModel.
type schemaField struct {
WireName string
Name string
Type string
DescriptionLines []string
Required bool
ReadOnly bool
Schema *base.SchemaProxy
}

// additionalPropertiesModel describes synthetic map storage used when object
Expand Down Expand Up @@ -239,6 +245,7 @@ func convertOperation(method, path string, item *v3.PathItem, op *v3.Operation,
HttpMethod: strings.ToUpper(method),
Path: path,
TagName: firstTag(op.Tags),
Operation: op,
}

params := collectParameters(item, op)
Expand Down Expand Up @@ -277,6 +284,7 @@ func convertOperation(method, path string, item *v3.PathItem, op *v3.Operation,
model.HasHeaderParams = len(model.RequiredHeaderParams) > 0 || len(model.OptionalHeaderParams) > 0

if op.RequestBody != nil {
model.RequestSchema = preferredSchema(op.RequestBody.Content)
model.RequestBodyType = schemaTypeFromContent(op.RequestBody, resolver, sanitizedID, "Request")
model.RequestRequired = op.RequestBody.Required != nil && *op.RequestBody.Required
model.RequestDescription = normalizeText(op.RequestBody.Description)
Expand Down Expand Up @@ -384,6 +392,8 @@ func filterParams(params []*v3.Parameter, location string, resolver *typeResolve
Required: required,
Type: javaType,
Location: location,
Schema: schemaRef,
Parameter: param,
})
}
sort.Slice(filtered, func(i, j int) bool {
Expand Down Expand Up @@ -568,7 +578,7 @@ func buildSchemas(doc *v3.Document, params Params, resolver *typeResolver) []sch
imports := sortedImports(map[string]struct{}{
"com.fasterxml.jackson.annotation.JsonCreator": {},
"com.fasterxml.jackson.annotation.JsonValue": {},
"java.util.Objects": {},
"java.util.Objects": {},
})
result = append(result, schemaModel{
Name: name,
Expand Down Expand Up @@ -655,11 +665,13 @@ func buildSchemaFields(name string, ref *base.SchemaProxy, resolver *typeResolve
desc = schemaFromProxy(propRef).Description
}
fields = append(fields, schemaField{
WireName: propName,
Name: camelCase(propName, propName),
Type: javaType.Name,
DescriptionLines: splitComment(desc),
Required: required[propName],
ReadOnly: readOnly,
Schema: propRef,
})
if required[propName] && !readOnly {
hasRequired = true
Expand Down
Loading
Loading