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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Update to okhttp `5.3.2`
- Requiring Java 21
- Remove implicit grant flow (see [blog post from Spotify](https://developer.spotify.com/blog/2025-10-14-reminder-oauth-migration-27-nov-2025))
- Add JSpecify nullness annotations

## [4.3.2]
- Remove `followers` property from `PlaylistUser` object
Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@

<!-- Dependencies -->
<lombok.version>1.18.42</lombok.version>
<jspecify.version>1.0.0</jspecify.version>

<!-- Test Dependencies -->
<junit.version>5.13.4</junit.version>
Expand Down
5 changes: 5 additions & 0 deletions spotify-web-api-java-generator/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
</properties>

<dependencies>
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>${jspecify.version}</version>
</dependency>
<dependency>
<groupId>io.swagger.parser.v3</groupId>
<artifactId>swagger-parser</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public class CLI implements Runnable {
boolean shouldClean = false;

@Option(names = {"-p", "--package"}, required = true, description = "The Java package name")
String packageName = null;
String packageName;

@Option(names = { "-h", "--help" }, usageHelp = true, description = "Print usage help")
boolean helpRequested = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import io.swagger.v3.oas.models.OpenAPI;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.nio.file.Path;

@Slf4j
Expand All @@ -17,7 +16,7 @@ public JavaGenerator() {
this.mustacheFactory = new NoEscapingMustacheFactory();
}

public void generate(OpenAPI openAPI, Path outputDirectory, JavaPackage javaPackage) throws IOException, GeneratorException {
public void generate(OpenAPI openAPI, Path outputDirectory, JavaPackage javaPackage) throws GeneratorException {
var generationContext = new GenerationContext(this.mustacheFactory, openAPI, javaPackage, outputDirectory);

new BaseObjectGenerator(generationContext).generateBaseObject();
Expand All @@ -30,8 +29,7 @@ public void generate(OpenAPI openAPI, Path outputDirectory, JavaPackage javaPack

EndpointSplitter.splitEndpoints(spotifyWebApi);

var apiTemplate = new ApiGenerator(generationContext);
apiTemplate.generateEndpoints(spotifyWebApi);
ApiGenerator.generateEndpoints(generationContext, spotifyWebApi);

new SpotifyWebApiGenerator(generationContext).generate();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import de.sonallux.spotify.generator.java.util.JavaPackage;
import de.sonallux.spotify.generator.java.util.JavaUtils;
import de.sonallux.spotify.generator.java.util.Markdown2Html;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;

import java.util.*;
Expand All @@ -17,16 +18,17 @@
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;

@RequiredArgsConstructor
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class ApiGenerator {
private final GenerationContext generationContext;

private JavaPackage apisJavaPackage;
private final JavaPackage apisJavaPackage;

public void generateEndpoints(SpotifyWebApi spotifyWebApi) {
apisJavaPackage = generationContext.childPackage("apis");
public static void generateEndpoints(GenerationContext generationContext, SpotifyWebApi spotifyWebApi) {
final var apisJavaPackage = generationContext.childPackage("apis");
final var apiGenerator = new ApiGenerator(generationContext, apisJavaPackage);

spotifyWebApi.getCategories().forEach(this::generateApiClasses);
spotifyWebApi.getCategories().forEach(apiGenerator::generateApiClasses);
}

private void generateApiClasses(ApiCategory category) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import io.swagger.v3.oas.models.media.ComposedSchema;
import io.swagger.v3.oas.models.media.ObjectSchema;
import io.swagger.v3.oas.models.media.Schema;
import org.jspecify.annotations.Nullable;

import java.nio.file.Path;
import java.util.HashMap;
Expand Down Expand Up @@ -41,6 +42,7 @@ public ObjectGenerator(GenerationContext generationContext) {
this.schemaNameToObjectName = new ConcurrentHashMap<>();
}

@Nullable
public String getObjectNameForResponse(String responseName) {
return responseNameToObjectName.get(responseName);
}
Expand Down Expand Up @@ -71,78 +73,80 @@ public void generateAllObjects() {
* @param schema the OpenAPI schema
* @return an object name to use for this schema
*/
private String generateApiObject(String openApiName, Schema schema) {
private String generateApiObject(String openApiName, Schema<?> schema) {
if (schema.get$ref() != null) {
var schemaName = OpenApiUtils.getSchemaName(schema.get$ref());
return getObjectNameOrGenerate(schemaName, generationContext.resolveSchema(schema.get$ref()));
}

var objectName = getObjectNameFromSchemaName(openApiName);

if (schema instanceof ObjectSchema objectSchema) {
var apiObject = generateApiObject(objectSchema, objectName);
apiObject.setOpenApiName(openApiName);
return objectName;
}
if (schema instanceof ArraySchema arraySchema) {
var itemsSchema = arraySchema.getItems();
var itemsType = JavaUtils.getTypeOfSchema(itemsSchema).orElse("Object");

return "java.util.List<" + itemsType + ">";
}
if (schema instanceof ComposedSchema composedSchema) {
if (composedSchema.getAllOf() != null) {
var allOf = composedSchema.getAllOf();
if (allOf.size() == 1) {
var ref = allOf.getFirst().get$ref();
if (ref.equals("#/components/schemas/PagingObject")) {
var itemsSchema = (ArraySchema) composedSchema.getProperties().get("items");
var itemsSchemaName = OpenApiUtils.getSchemaName(itemsSchema.getItems().get$ref());
var itemsObjectName = getObjectNameOrGenerate(itemsSchemaName, itemsSchema.getItems());
return "Paging<" + itemsObjectName + ">";
}

var referencedSchemaName = OpenApiUtils.getSchemaName(ref);
var referencedObjectName = getObjectNameOrGenerate(referencedSchemaName, allOf.getFirst());
var apiObject = ApiObject.builder()
switch (schema) {
case ObjectSchema objectSchema -> {
var apiObject = generateApiObject(objectSchema, objectName);
apiObject.setOpenApiName(openApiName);
return objectName;
}
case ArraySchema arraySchema -> {
var itemsSchema = arraySchema.getItems();
var itemsType = JavaUtils.getTypeOfSchema(itemsSchema).orElse("Object");
return "java.util.List<" + itemsType + ">";
}
case ComposedSchema composedSchema -> {
if (composedSchema.getAllOf() != null) {
var allOf = composedSchema.getAllOf();
if (allOf.size() == 1) {
var ref = allOf.getFirst().get$ref();
if (ref.equals("#/components/schemas/PagingObject")) {
var itemsSchema = (ArraySchema) composedSchema.getProperties().get("items");
var itemsSchemaName = OpenApiUtils.getSchemaName(itemsSchema.getItems().get$ref());
var itemsObjectName = getObjectNameOrGenerate(itemsSchemaName, itemsSchema.getItems());
return "Paging<" + itemsObjectName + ">";
}

var referencedSchemaName = OpenApiUtils.getSchemaName(ref);
var referencedObjectName = getObjectNameOrGenerate(referencedSchemaName, allOf.getFirst());
var apiObject = ApiObject.builder()
.name(objectName)
.openApiName(openApiName)
.superClassName(referencedObjectName)
.description(composedSchema.getDescription())
.build();
this.schemaObjects.put(objectName, apiObject);
return objectName;
}
if (allOf.size() == 2) {
if (allOf.get(0).get$ref().equals("#/components/schemas/PagingObject")) {
var itemsSchema = (ArraySchema) allOf.get(1).getProperties().get("items");
var itemsSchemaName = OpenApiUtils.getSchemaName(itemsSchema.getItems().get$ref());
var itemsObjectName = getObjectNameOrGenerate(itemsSchemaName, itemsSchema.getItems());
return "Paging<" + itemsObjectName + ">";
}
if (allOf.get(0).get$ref().equals("#/components/schemas/CursorPagingObject")) {
var itemsSchema = (ArraySchema) allOf.get(1).getProperties().get("items");
var itemsSchemaName = OpenApiUtils.getSchemaName(itemsSchema.getItems().get$ref());
var itemsObjectName = getObjectNameOrGenerate(itemsSchemaName, itemsSchema.getItems());
return "CursorPaging<" + itemsObjectName + ">";
}

if (allOf.get(1) instanceof ObjectSchema objectSchema) {
var referencedSchemaName = OpenApiUtils.getSchemaName(allOf.get(0).get$ref());
var referencedObjectName = getObjectNameOrGenerate(referencedSchemaName, allOf.get(0));

var apiObject = generateApiObject(objectSchema, objectName);
apiObject.setDescription(composedSchema.getDescription());
apiObject.setOpenApiName(openApiName);
apiObject.setSuperClassName(referencedObjectName);
this.schemaObjects.put(objectName, apiObject);
return objectName;
}
if (allOf.size() == 2) {
if (allOf.get(0).get$ref().equals("#/components/schemas/PagingObject")) {
var itemsSchema = (ArraySchema) allOf.get(1).getProperties().get("items");
var itemsSchemaName = OpenApiUtils.getSchemaName(itemsSchema.getItems().get$ref());
var itemsObjectName = getObjectNameOrGenerate(itemsSchemaName, itemsSchema.getItems());
return "Paging<" + itemsObjectName + ">";
}
if (allOf.get(0).get$ref().equals("#/components/schemas/CursorPagingObject")) {
var itemsSchema = (ArraySchema) allOf.get(1).getProperties().get("items");
var itemsSchemaName = OpenApiUtils.getSchemaName(itemsSchema.getItems().get$ref());
var itemsObjectName = getObjectNameOrGenerate(itemsSchemaName, itemsSchema.getItems());
return "CursorPaging<" + itemsObjectName + ">";
}

if (allOf.get(1) instanceof ObjectSchema objectSchema) {
var referencedSchemaName = OpenApiUtils.getSchemaName(allOf.getFirst().get$ref());
var referencedObjectName = getObjectNameOrGenerate(referencedSchemaName, allOf.getFirst());

var apiObject = generateApiObject(objectSchema, objectName);
apiObject.setDescription(composedSchema.getDescription());
apiObject.setOpenApiName(openApiName);
apiObject.setSuperClassName(referencedObjectName);
return objectName;
}
}
}
return objectName;
}
default -> {
return objectName;
}
}


return objectName;
}

private ApiObject generateApiObject(ObjectSchema objectSchema, String objectName) {
Expand Down Expand Up @@ -180,7 +184,7 @@ private ApiObject.Property generateApiObjectProperty(String objectName, String n
return new ApiObject.Property(name, type, resolvedSchema.getDescription());
}

private String getObjectNameOrGenerate(String openApiName, Schema schema) {
private String getObjectNameOrGenerate(String openApiName, Schema<?> schema) {
if (schemaNameToObjectName.containsKey(openApiName)) {
return schemaNameToObjectName.get(openApiName);
}
Expand Down Expand Up @@ -260,7 +264,7 @@ private void fixContextForPaging(Map<String, Object> context) {
));
}

private static String firstNonNull(String... strings) {
private static @Nullable String firstNonNull(@Nullable String... strings) {
for (var s : strings) {
if (s != null) {
return s;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.responses.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.jspecify.annotations.Nullable;

import java.util.function.Function;

@RequiredArgsConstructor
public class ObjectModelCreator {
private final GenerationContext generationContext;
private final Function<String, String> responseTypeMapper;
private final Function<String, @Nullable String> responseTypeMapper;

private SpotifyWebApi spotifyWebApi;
private SpotifyWebApi spotifyWebApi = new SpotifyWebApi();

public SpotifyWebApi createSpotifyWebApiModel(OpenAPI openAPI) {
spotifyWebApi = new SpotifyWebApi();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@NullMarked
package de.sonallux.spotify.generator.java.generators;

import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import de.sonallux.spotify.generator.java.util.Markdown2Html;
import lombok.Getter;
import lombok.Setter;
import org.jspecify.annotations.Nullable;

import java.util.ArrayList;
import java.util.List;
Expand All @@ -25,7 +26,7 @@ public class ApiEndpoint {
private final List<Parameter> optionalPathParameters;
private final List<Parameter> optionalQueryParameters;
private final List<Parameter> optionalBodyParameters;
private RawBodyParameter rawBodyParameter = null;
private @Nullable RawBodyParameter rawBodyParameter = null;

public ApiEndpoint(String endpointId, String name, String description, String path, String httpMethod, String responseType, String responseDescription, List<String> scopes, boolean deprecated) {
this.endpointId = endpointId;
Expand Down Expand Up @@ -72,7 +73,7 @@ public static class Parameter {
private String type;
private boolean commaSeparatedListType;
private String description;
private String defaultValue;
private @Nullable String defaultValue;

public Parameter(String name, String type, String description) {
this.name = name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NonNull;
import org.jspecify.annotations.Nullable;

import java.util.Comparator;
import java.util.HashMap;
Expand All @@ -17,11 +17,10 @@ public class ApiObject {
/**
* Name of the schema object from OpenAPI. Is null if this object is extracted from a complex OpenAPI schema
*/
private String openApiName;
@NonNull
private @Nullable String openApiName;
private String name;
private String description;
private String superClassName;
private @Nullable String description;
private @Nullable String superClassName;
@Builder.Default
private Map<String, Property> properties = new HashMap<>();

Expand All @@ -39,10 +38,9 @@ public List<Property> getPropertyList() {
@Data
@AllArgsConstructor
public static class Property {
@NonNull
private String name;
@NonNull
private String type;
@Nullable
private String description;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@NullMarked
package de.sonallux.spotify.generator.java.model;

import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@NullMarked
package de.sonallux.spotify.generator.java;

import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import lombok.EqualsAndHashCode;
import org.jspecify.annotations.Nullable;

import java.nio.file.Path;
import java.util.Arrays;
Expand All @@ -15,18 +16,18 @@ private JavaPackage(String... packageNames) {
this.packageNames = packageNames;
}

public static JavaPackage fromNames(String... packageNames) {
public static JavaPackage fromNames(@Nullable String... packageNames) {
Preconditions.checkArgument(isValidJavaPackageName(packageNames), "Invalid java package");
return new JavaPackage(packageNames);
}

public static JavaPackage fromPackage(String packageName) {
public static JavaPackage fromPackage(@Nullable String packageName) {
Preconditions.checkArgument(packageName != null);
var packageNames = packageName.split("\\.");
return fromNames(packageNames);
}

private static boolean isValidJavaPackageName(String[] packageNames) {
private static boolean isValidJavaPackageName(@Nullable String[] packageNames) {
if (packageNames.length == 0) {
return false;
}
Expand Down
Loading