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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ String readerId =
() ->
client.readers().list(merchantCode).items().stream()
.findFirst()
.map(reader -> reader.id().value())
.map(reader -> reader.id())
.orElseThrow(() -> new IllegalStateException("No paired readers found.")));

CreateReaderCheckoutRequest request =
Expand Down Expand Up @@ -224,7 +224,7 @@ CompletableFuture<String> readerIdFuture =
response ->
response.items().stream()
.findFirst()
.map(reader -> reader.id().value())
.map(reader -> reader.id())
.orElseThrow(
() ->
new IllegalStateException("No paired readers found."))));
Expand Down
6 changes: 6 additions & 0 deletions codegen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ just generate

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.

Named scalar schemas use the same Java types as inline schemas: `String`, boxed
numbers and booleans, or the existing date/time and UUID format mappings. They do
not generate wrapper records. Enum schemas retain their open enum classes, and
structured schemas retain their models. This applies to parameters, request and
response fields, and code samples.

## 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.
Expand Down
11 changes: 1 addition & 10 deletions codegen/internal/generator/additional_properties_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,7 @@ func TestGenerateModelWithoutBuilderForTypeAliases(t *testing.T) {
}

lonPath := filepath.Join(outputDir, "com", "test", "sdk", "models", "Lon.java")
lonContent, err := os.ReadFile(lonPath)
if err != nil {
t.Fatalf("read generated Lon model: %v", err)
}
lonGenerated := string(lonContent)

assertContains(t, lonGenerated, "public record Lon(")
assertContains(t, lonGenerated, "Float value")
assertNotContains(t, lonGenerated, "public static Builder builder()")
assertNotContains(t, lonGenerated, "public static final class Builder")
assertFileDoesNotExist(t, lonPath)

metaPath := filepath.Join(outputDir, "com", "test", "sdk", "models", "Meta.java")
metaContent, err := os.ReadFile(metaPath)
Expand Down
4 changes: 2 additions & 2 deletions codegen/internal/generator/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,7 @@ func buildSchemas(doc *v3.Document, params Params, resolver *typeResolver) []sch
result := make([]schemaModel, 0, len(names))
for _, name := range names {
ref := doc.Components.Schemas.GetOrZero(name)
if ref == nil {
if ref == nil || isPlainScalarSchema(ref.Schema()) {
continue
}
description := schemaDescription(ref)
Expand Down Expand Up @@ -617,7 +617,7 @@ func buildSchemas(doc *v3.Document, params Params, resolver *typeResolver) []sch
}

// shouldGenerateBuilder reports whether the model should expose a builder.
// Single-field wrapper records (for example Lon/Lat/Meta-style aliases) don't
// Single-field wrapper records (for example map aliases) don't
// benefit from a builder and should use the canonical record constructor.
func shouldGenerateBuilder(fields []schemaField, additionalProps *additionalPropertiesModel) bool {
if additionalProps != nil {
Expand Down
3 changes: 0 additions & 3 deletions codegen/internal/generator/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,6 @@ func renderSumUpClient(model sdkModel, params Params) error {

// renderModels generates POJO classes that mirror OpenAPI schemas.
func renderModels(model sdkModel, params Params) error {
if len(model.Schemas) == 0 {
return nil
}
dir := filepath.Join(params.OutputDir, params.modelPackagePath())
if err := os.RemoveAll(dir); err != nil {
return fmt.Errorf("remove models directory: %w", err)
Expand Down
115 changes: 115 additions & 0 deletions codegen/internal/generator/scalars_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package generator

import (
"os"
"path/filepath"
"testing"
)

func TestGenerateScalarReferences(t *testing.T) {
t.Parallel()

tmp := t.TempDir()
specPath := filepath.Join(tmp, "openapi.yaml")
spec := `openapi: 3.1.0
info: {title: Scalars, version: '1'}
paths:
/readers/{id}:
post:
operationId: createReader
x-codegen: {method_name: create}
tags: [Readers]
parameters:
- name: id
in: path
required: true
schema: {$ref: '#/components/schemas/Name'}
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/Request'}
responses:
'200':
description: Name
content:
application/json:
schema: {$ref: '#/components/schemas/Name'}
components:
schemas:
Name: {type: string, description: Reader display name., example: Counter 1}
Count: {type: integer, format: int32}
Total: {type: integer, format: int64}
Latitude: {type: number, format: float}
Amount: {type: number, format: double}
Enabled: {type: boolean}
Date: {type: string, format: date}
Timestamp: {type: string, format: date-time}
Identifier: {type: string, format: uuid}
NullableName: {type: [string, 'null']}
Status: {type: string, enum: [active, inactive]}
Request:
type: object
required: [name, nullableName]
properties:
name: {$ref: '#/components/schemas/Name'}
nullableName: {$ref: '#/components/schemas/NullableName'}
count: {$ref: '#/components/schemas/Count'}
total: {$ref: '#/components/schemas/Total'}
latitude: {$ref: '#/components/schemas/Latitude'}
amount: {$ref: '#/components/schemas/Amount'}
enabled: {$ref: '#/components/schemas/Enabled'}
date: {$ref: '#/components/schemas/Date'}
timestamp: {$ref: '#/components/schemas/Timestamp'}
identifier: {$ref: '#/components/schemas/Identifier'}
status: {$ref: '#/components/schemas/Status'}
names:
type: array
items: {$ref: '#/components/schemas/Name'}
counts:
type: object
additionalProperties: {$ref: '#/components/schemas/Count'}
`
if err := os.WriteFile(specPath, []byte(spec), 0o600); err != nil {
t.Fatal(err)
}
params := Params{SpecPath: specPath, OutputDir: filepath.Join(tmp, "java"), ResourceDir: filepath.Join(tmp, "resources")}
if err := Run(t.Context(), params); err != nil {
t.Fatal(err)
}
read := func(path string) string {
t.Helper()
data, err := os.ReadFile(filepath.Join(params.OutputDir, "com/sumup/sdk", path))
if err != nil {
t.Fatal(err)
}
return string(data)
}
request := read("models/Request.java")
for _, field := range []string{
"String name", "String nullableName", "Integer count", "Long total", "Float latitude", "Double amount",
"Boolean enabled", "java.time.LocalDate date", "java.time.OffsetDateTime timestamp", "java.util.UUID identifier",
"com.sumup.sdk.models.Status status", "java.util.List<String> names", "java.util.Map<String, Integer> counts",
"Reader display name.", `Objects.requireNonNull(name, "name")`,
} {
assertContains(t, request, field)
}
assertNotContains(t, request, `Objects.requireNonNull(nullableName`)
for _, name := range []string{"Name", "NullableName", "Count", "Total", "Latitude", "Amount", "Enabled", "Date", "Timestamp", "Identifier"} {
assertFileDoesNotExist(t, filepath.Join(params.OutputDir, "com/sumup/sdk/models", name+".java"))
}
assertContains(t, read("models/Status.java"), "public static final Status ACTIVE")
assertContains(t, read("clients/ReadersClient.java"), "public String create(")
assertContains(t, read("clients/ReadersClient.java"), "String id")
assertContains(t, read("clients/ReadersAsyncClient.java"), "CompletableFuture<String> create(")

catalog, err := BuildSamples(params, "test")
if err != nil {
t.Fatal(err)
}
if len(catalog.Samples) != 1 {
t.Fatalf("expected one sample, got %d", len(catalog.Samples))
}
assertContains(t, catalog.Samples[0].Source, `.name("Counter 1")`)
assertNotContains(t, catalog.Samples[0].Source, "new com.sumup.sdk.models.Name")
}
31 changes: 28 additions & 3 deletions codegen/internal/generator/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ func newTypeResolver(doc *v3.Document, params Params) *typeResolver {
inlineNameUsage: make(map[string]int),
}
if doc.Components != nil && doc.Components.Schemas != nil && doc.Components.Schemas.Len() > 0 {
for name := range doc.Components.Schemas.KeysFromOldest() {
for name, ref := range doc.Components.Schemas.FromOldest() {
if isPlainScalarSchema(schemaFromProxy(ref)) {
continue
}
className := pascalCase(name, "")
resolver.schemaTypes[name] = params.modelPackage() + "." + className
resolver.inlineNameUsage[className]++
Expand Down Expand Up @@ -76,7 +79,7 @@ func (r *typeResolver) javaType(ref *base.SchemaProxy, context ...string) javaTy
if ref == nil {
return r.genericMap()
}
if ref.IsReference() {
if ref.IsReference() && !isPlainScalarSchema(ref.Schema()) {
name := componentNameFromRef(ref.GetReference())
if name != "" {
fqn := r.schemaClassName(name)
Expand Down Expand Up @@ -172,6 +175,28 @@ func (r *typeResolver) parameterJavaType(ref *base.SchemaProxy, context ...strin
return r.javaType(ref, context...)
}

// isPlainScalarSchema identifies schemas that use Java's scalar types even when
// named as components. Enums and composed schemas retain their model handling.
func isPlainScalarSchema(schema *base.Schema) bool {
if schema == nil || len(schema.Enum) > 0 || len(schema.AllOf) > 0 || len(schema.OneOf) > 0 || len(schema.AnyOf) > 0 {
return false
}
scalar := false
for _, kind := range schema.Type {
switch kind {
case "string", "integer", "number", "boolean":
if scalar {
return false
}
scalar = true
case "null":
default:
return false
}
}
return scalar
}

// objectType handles schemas that look like objects by either emitting inline
// models or falling back to generic map types.
func (r *typeResolver) objectType(schema *base.Schema, context []string) javaType {
Expand Down Expand Up @@ -330,7 +355,7 @@ func (r *typeResolver) inlineSchemaModels(params Params) []schemaModel {
imports := sortedImports(map[string]struct{}{
"com.fasterxml.jackson.annotation.JsonCreator": {},
"com.fasterxml.jackson.annotation.JsonValue": {},
"java.util.Objects": {},
"java.util.Objects": {},
})
models = append(models, schemaModel{
Name: info.className,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ public static void main(String[] args) {
SumUpClient client = new SumUpClient();

Optional<String> readerId =
client.readers().list(merchantCode).items().stream()
.findFirst()
.map(reader -> reader.id().value());
client.readers().list(merchantCode).items().stream().findFirst().map(reader -> reader.id());
if (readerId.isEmpty()) {
System.err.println("Merchant has no paired readers.");
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public static final class ListMembershipsQueryParams {
* @param value Filter memberships by resource kind.
* @return This ListMembershipsQueryParams instance.
*/
public ListMembershipsQueryParams kind(com.sumup.sdk.models.ResourceType value) {
public ListMembershipsQueryParams kind(String value) {
this.values.put("kind", Objects.requireNonNull(value, "kind"));
return this;
}
Expand Down Expand Up @@ -193,7 +193,7 @@ public ListMembershipsQueryParams resourceParentType(java.util.Map<String, Objec
* @param value Filter memberships by resource kind.
* @return This ListMembershipsQueryParams instance.
*/
public ListMembershipsQueryParams resourceType(com.sumup.sdk.models.ResourceType value) {
public ListMembershipsQueryParams resourceType(String value) {
this.values.put("resource.type", Objects.requireNonNull(value, "resourceType"));
return this;
}
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/sumup/sdk/clients/MembershipsClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public static final class ListMembershipsQueryParams {
* @param value Filter memberships by resource kind.
* @return This ListMembershipsQueryParams instance.
*/
public ListMembershipsQueryParams kind(com.sumup.sdk.models.ResourceType value) {
public ListMembershipsQueryParams kind(String value) {
this.values.put("kind", Objects.requireNonNull(value, "kind"));
return this;
}
Expand Down Expand Up @@ -188,7 +188,7 @@ public ListMembershipsQueryParams resourceParentType(java.util.Map<String, Objec
* @param value Filter memberships by resource kind.
* @return This ListMembershipsQueryParams instance.
*/
public ListMembershipsQueryParams resourceType(com.sumup.sdk.models.ResourceType value) {
public ListMembershipsQueryParams resourceType(String value) {
this.values.put("resource.type", Objects.requireNonNull(value, "resourceType"));
return this;
}
Expand Down
27 changes: 10 additions & 17 deletions src/main/java/com/sumup/sdk/clients/ReadersAsyncClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,7 @@ public CompletableFuture<com.sumup.sdk.models.CreateReaderCheckoutResponse> crea
* @throws ApiException if the SumUp API returns an error.
*/
public CompletableFuture<com.sumup.sdk.models.ReaderPaymentResponse> createGoCheckout(
String merchantCode,
com.sumup.sdk.models.ReaderId readerId,
com.sumup.sdk.models.ReaderPaymentRequestParams request)
String merchantCode, String readerId, com.sumup.sdk.models.ReaderPaymentRequestParams request)
throws ApiException {
return createGoCheckout(merchantCode, readerId, request, null);
}
Expand All @@ -205,7 +203,7 @@ public CompletableFuture<com.sumup.sdk.models.ReaderPaymentResponse> createGoChe
*/
public CompletableFuture<com.sumup.sdk.models.ReaderPaymentResponse> createGoCheckout(
String merchantCode,
com.sumup.sdk.models.ReaderId readerId,
String readerId,
com.sumup.sdk.models.ReaderPaymentRequestParams request,
RequestOptions requestOptions)
throws ApiException {
Expand Down Expand Up @@ -242,8 +240,7 @@ public CompletableFuture<com.sumup.sdk.models.ReaderPaymentResponse> createGoChe
* @return CompletableFuture completed when the request finishes.
* @throws ApiException if the SumUp API returns an error.
*/
public CompletableFuture<Void> delete(String merchantCode, com.sumup.sdk.models.ReaderId readerId)
throws ApiException {
public CompletableFuture<Void> delete(String merchantCode, String readerId) throws ApiException {
return delete(merchantCode, readerId, null);
}

Expand All @@ -262,8 +259,7 @@ public CompletableFuture<Void> delete(String merchantCode, com.sumup.sdk.models.
* @throws ApiException if the SumUp API returns an error.
*/
public CompletableFuture<Void> delete(
String merchantCode, com.sumup.sdk.models.ReaderId readerId, RequestOptions requestOptions)
throws ApiException {
String merchantCode, String readerId, RequestOptions requestOptions) throws ApiException {
Objects.requireNonNull(merchantCode, "merchantCode");
Objects.requireNonNull(readerId, "readerId");
String path = "/v0.1/merchants/{merchant_code}/readers/{reader_id}";
Expand All @@ -290,8 +286,8 @@ public CompletableFuture<Void> delete(
* @return CompletableFuture resolved with com.sumup.sdk.models.Reader parsed response.
* @throws ApiException if the SumUp API returns an error.
*/
public CompletableFuture<com.sumup.sdk.models.Reader> get(
String merchantCode, com.sumup.sdk.models.ReaderId readerId) throws ApiException {
public CompletableFuture<com.sumup.sdk.models.Reader> get(String merchantCode, String readerId)
throws ApiException {
return get(merchantCode, readerId, null);
}

Expand All @@ -311,8 +307,7 @@ public CompletableFuture<com.sumup.sdk.models.Reader> get(
* @throws ApiException if the SumUp API returns an error.
*/
public CompletableFuture<com.sumup.sdk.models.Reader> get(
String merchantCode, com.sumup.sdk.models.ReaderId readerId, GetReaderHeaders getReader)
throws ApiException {
String merchantCode, String readerId, GetReaderHeaders getReader) throws ApiException {
return get(merchantCode, readerId, getReader, null);
}

Expand All @@ -333,7 +328,7 @@ public CompletableFuture<com.sumup.sdk.models.Reader> get(
*/
public CompletableFuture<com.sumup.sdk.models.Reader> get(
String merchantCode,
com.sumup.sdk.models.ReaderId readerId,
String readerId,
GetReaderHeaders getReader,
RequestOptions requestOptions)
throws ApiException {
Expand Down Expand Up @@ -623,9 +618,7 @@ public CompletableFuture<Void> terminateCheckout(
* @throws ApiException if the SumUp API returns an error.
*/
public CompletableFuture<com.sumup.sdk.models.Reader> update(
String merchantCode,
com.sumup.sdk.models.ReaderId readerId,
com.sumup.sdk.models.UpdateReaderRequest request)
String merchantCode, String readerId, com.sumup.sdk.models.UpdateReaderRequest request)
throws ApiException {
return update(merchantCode, readerId, request, null);
}
Expand All @@ -647,7 +640,7 @@ public CompletableFuture<com.sumup.sdk.models.Reader> update(
*/
public CompletableFuture<com.sumup.sdk.models.Reader> update(
String merchantCode,
com.sumup.sdk.models.ReaderId readerId,
String readerId,
com.sumup.sdk.models.UpdateReaderRequest request,
RequestOptions requestOptions)
throws ApiException {
Expand Down
Loading
Loading