diff --git a/codegen/internal/generator/params.go b/codegen/internal/generator/params.go index 12a4c15..23401b5 100644 --- a/codegen/internal/generator/params.go +++ b/codegen/internal/generator/params.go @@ -12,6 +12,7 @@ import ( type Params struct { SpecPath string OutputDir string + ResourceDir string BasePackage string } @@ -34,6 +35,12 @@ func (p *Params) normalize() error { if p.OutputDir, err = filepath.Abs(p.OutputDir); err != nil { return fmt.Errorf("resolve output directory: %w", err) } + if p.ResourceDir == "" { + p.ResourceDir = filepath.Join(filepath.Dir(p.OutputDir), "resources") + } + if p.ResourceDir, err = filepath.Abs(p.ResourceDir); err != nil { + return fmt.Errorf("resolve resource directory: %w", err) + } return nil } @@ -55,6 +62,18 @@ func (p *Params) validate() error { } else if !fi.IsDir() { return fmt.Errorf("output directory %s is not a directory", p.OutputDir) } + fi, err = os.Stat(p.ResourceDir) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + if err := os.MkdirAll(p.ResourceDir, 0o755); err != nil { + return fmt.Errorf("create resource directory: %w", err) + } + } else { + return fmt.Errorf("resource directory: %w", err) + } + } else if !fi.IsDir() { + return fmt.Errorf("resource directory %s is not a directory", p.ResourceDir) + } return nil } diff --git a/codegen/internal/generator/resources.go b/codegen/internal/generator/resources.go new file mode 100644 index 0000000..4aa9df3 --- /dev/null +++ b/codegen/internal/generator/resources.go @@ -0,0 +1,24 @@ +package generator + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +func renderApiVersionResource(apiVersion string, params Params) error { + apiVersion = strings.TrimSpace(apiVersion) + if apiVersion == "" { + return fmt.Errorf("missing api version in spec info") + } + targetDir := filepath.Join(params.ResourceDir, params.basePackagePath()) + if err := os.MkdirAll(targetDir, 0o755); err != nil { + return fmt.Errorf("create api version resource directory: %w", err) + } + target := filepath.Join(targetDir, "api-version.txt") + if err := os.WriteFile(target, []byte(apiVersion), 0o644); err != nil { + return fmt.Errorf("write api version resource: %w", err) + } + return nil +} diff --git a/codegen/internal/generator/run.go b/codegen/internal/generator/run.go index 16445e6..7e66ecf 100644 --- a/codegen/internal/generator/run.go +++ b/codegen/internal/generator/run.go @@ -32,6 +32,11 @@ func Run(ctx context.Context, params Params) error { return err } + apiVersion := "" + if doc.Info != nil { + apiVersion = doc.Info.Version + } + slog.Info("Generating SDK", "spec", params.SpecPath) if err := renderClients(model, params); err != nil { return err @@ -42,6 +47,9 @@ func Run(ctx context.Context, params Params) error { if err := renderSumUpClient(model, params); err != nil { return err } + if err := renderApiVersionResource(apiVersion, params); err != nil { + return err + } return nil } diff --git a/src/build.gradle b/src/build.gradle index 269f448..2965614 100644 --- a/src/build.gradle +++ b/src/build.gradle @@ -21,7 +21,7 @@ sourceSets { def emptyDirs = [] main { java.srcDirs = ['main/java'] - resources.srcDirs = emptyDirs + resources.srcDirs = ['main/resources'] } test { java.srcDirs = ['test/java'] diff --git a/src/main/java/com/sumup/sdk/core/ApiClient.java b/src/main/java/com/sumup/sdk/core/ApiClient.java index 1c33bac..5756cf1 100644 --- a/src/main/java/com/sumup/sdk/core/ApiClient.java +++ b/src/main/java/com/sumup/sdk/core/ApiClient.java @@ -186,6 +186,7 @@ private void applyHeaders( RequestOptions requestOptions) { Map merged = new LinkedHashMap<>(); merged.put("User-Agent", SdkMetadata.userAgent()); + merged.putAll(SdkMetadata.runtimeHeaders()); if (headerParams != null) { headerParams.forEach( (name, value) -> { diff --git a/src/main/java/com/sumup/sdk/core/SdkMetadata.java b/src/main/java/com/sumup/sdk/core/SdkMetadata.java index d4eaa79..3b3141c 100644 --- a/src/main/java/com/sumup/sdk/core/SdkMetadata.java +++ b/src/main/java/com/sumup/sdk/core/SdkMetadata.java @@ -3,20 +3,38 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.Map; /** * Provides metadata about the SDK that can be attached to outgoing requests, such as the current * version and the default {@code User-Agent} header value. */ public final class SdkMetadata { + private static final String API_VERSION_RESOURCE = "/com/sumup/sdk/api-version.txt"; private static final String VERSION_RESOURCE = "/com/sumup/sdk/sdk-version.txt"; private static final String USER_AGENT_PREFIX = "sumup-java"; + private static final String LANGUAGE = "java"; - private static final String VERSION = loadVersion(); + private static final String API_VERSION = loadResource(API_VERSION_RESOURCE); + private static final String VERSION = loadResource(VERSION_RESOURCE); private static final String USER_AGENT = USER_AGENT_PREFIX + "/v" + VERSION; + private static final Map RUNTIME_HEADERS = + Map.of( + "X-Sumup-Api-Version", API_VERSION, + "X-Sumup-Lang", LANGUAGE, + "X-Sumup-Package-Version", VERSION, + "X-Sumup-OS", System.getProperty("os.name", "unknown"), + "X-Sumup-Arch", runtimeArch(), + "X-Sumup-Runtime", runtimeIdentifier(), + "X-Sumup-Runtime-Version", Runtime.version().toString()); private SdkMetadata() {} + /** Returns the API version declared by the OpenAPI specification. */ + public static String apiVersion() { + return API_VERSION; + } + /** Returns the SDK version read from the generated version resource. */ public static String version() { return VERSION; @@ -27,8 +45,28 @@ public static String userAgent() { return USER_AGENT; } - private static String loadVersion() { - try (InputStream stream = SdkMetadata.class.getResourceAsStream(VERSION_RESOURCE)) { + /** Returns the runtime headers that should be sent with each request. */ + public static Map runtimeHeaders() { + return RUNTIME_HEADERS; + } + + static String runtimeArch() { + String arch = System.getProperty("os.arch", "unknown").toLowerCase(); + return switch (arch) { + case "amd64", "x86_64" -> "x86_64"; + case "x86", "i386", "i486", "i586", "i686" -> "x86"; + case "aarch64", "arm64" -> "arm64"; + case "arm", "armv7", "armv7l" -> "arm"; + default -> arch; + }; + } + + private static String runtimeIdentifier() { + return LANGUAGE + Runtime.version(); + } + + private static String loadResource(String path) { + try (InputStream stream = SdkMetadata.class.getResourceAsStream(path)) { if (stream == null) { return "unknown"; } diff --git a/src/main/resources/com/sumup/sdk/api-version.txt b/src/main/resources/com/sumup/sdk/api-version.txt new file mode 100644 index 0000000..afaf360 --- /dev/null +++ b/src/main/resources/com/sumup/sdk/api-version.txt @@ -0,0 +1 @@ +1.0.0 \ No newline at end of file diff --git a/src/test/java/com/sumup/sdk/core/ApiClientTest.java b/src/test/java/com/sumup/sdk/core/ApiClientTest.java index b9cb5d4..288e1ee 100644 --- a/src/test/java/com/sumup/sdk/core/ApiClientTest.java +++ b/src/test/java/com/sumup/sdk/core/ApiClientTest.java @@ -74,6 +74,27 @@ void requestOptionsCanOverrideUserAgent() { "custom/agent", httpClient.lastRequest().headers().firstValue("User-Agent").orElse(null)); } + @Test + void defaultRuntimeHeadersAreIncluded() { + CapturingHttpClient httpClient = new CapturingHttpClient(); + ApiClient client = ApiClient.builder().httpClient(httpClient).build(); + + client.send(HttpMethod.GET, "/v1/test", null, null, null, null, null); + + HttpHeaders headers = httpClient.lastRequest().headers(); + assertEquals(SdkMetadata.apiVersion(), headers.firstValue("X-Sumup-Api-Version").orElse(null)); + assertEquals("java", headers.firstValue("X-Sumup-Lang").orElse(null)); + assertEquals(SdkMetadata.version(), headers.firstValue("X-Sumup-Package-Version").orElse(null)); + assertEquals( + System.getProperty("os.name", "unknown"), headers.firstValue("X-Sumup-OS").orElse(null)); + assertEquals( + SdkMetadata.runtimeHeaders().get("X-Sumup-Arch"), + headers.firstValue("X-Sumup-Arch").orElse(null)); + assertEquals("java" + Runtime.version(), headers.firstValue("X-Sumup-Runtime").orElse(null)); + assertEquals( + Runtime.version().toString(), headers.firstValue("X-Sumup-Runtime-Version").orElse(null)); + } + @Test void requestOptionsCanOverrideTimeout() { CapturingHttpClient httpClient = new CapturingHttpClient();