@redocly/cli 2.71.1 → 2.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/README.md +1 -1
  2. package/THIRD_PARTY_NOTICES +29 -7
  3. package/lib/chunks/{Q3IJURM3.js → 2DXUH2SS.js} +1 -1
  4. package/lib/chunks/{UYZXI3NO.js → 2X2KHSF3.js} +1 -1
  5. package/lib/chunks/{CX76LEFC.js → 42X6IAVM.js} +1 -1
  6. package/lib/chunks/44US4AGQ.js +13 -0
  7. package/lib/chunks/{DP3T6VZX.js → 4AXMK6B2.js} +1 -1
  8. package/lib/chunks/4ZCKAKVF.js +199 -0
  9. package/lib/chunks/5F7XFPS7.js +8 -0
  10. package/lib/chunks/{WJ3YDHQA.js → 6234PJEZ.js} +1 -1
  11. package/lib/chunks/{TNRIBQHV.js → 62SOICN7.js} +1 -1
  12. package/lib/chunks/{BPSWCUZE.js → 6JFL6L56.js} +1 -1
  13. package/lib/chunks/{NEWT5A6S.js → 6RTITFQA.js} +1 -1
  14. package/lib/chunks/{2IUGP2IH.js → 6XDKNFYM.js} +3 -3
  15. package/lib/chunks/{IYV7EO2J.js → 76AJ5P4Y.js} +1 -1
  16. package/lib/chunks/{6TCEQRVX.js → 7HTKK26B.js} +1 -1
  17. package/lib/chunks/{BXXX3ZCV.js → A7XJ7Y3N.js} +1 -1
  18. package/lib/chunks/AB4FYNJN.js +22 -0
  19. package/lib/chunks/BJ6Z66KZ.js +7 -0
  20. package/lib/chunks/{3N7LKSDD.js → BL2KWYMY.js} +1 -1
  21. package/lib/chunks/{PBQOGZ5C.js → BTLP7Z6A.js} +1 -1
  22. package/lib/chunks/{5DW3QRJB.js → BXMYSHQY.js} +2 -2
  23. package/lib/chunks/{ILYXMZRT.js → BY22HEXP.js} +1 -1
  24. package/lib/chunks/D2BBHPMR.js +7 -0
  25. package/lib/chunks/{ACAPGKSR.js → D3C6XIMY.js} +3 -3
  26. package/lib/chunks/{INGAWMLB.js → DNDYWJHC.js} +1 -1
  27. package/lib/chunks/{KJIUDCYZ.js → E6QTRG6I.js} +1 -1
  28. package/lib/chunks/EHMG7I34.js +9 -0
  29. package/lib/chunks/{MH5X2EVH.js → EL6MGCYE.js} +1 -1
  30. package/lib/chunks/EVCSNWMN.js +7 -0
  31. package/lib/chunks/{RBNQC5ZB.js → FJBULQ4P.js} +1 -1
  32. package/lib/chunks/{Y5DDX2MU.js → FLL7MTDS.js} +1 -1
  33. package/lib/chunks/{X7C43UBI.js → FM7Y6KMP.js} +1 -1
  34. package/lib/chunks/{ZQTHHPK3.js → FMMOQ6SE.js} +2 -2
  35. package/lib/chunks/{FBEEMKTB.js → FRC2YJS4.js} +1 -1
  36. package/lib/chunks/{5SSWNONM.js → GHJGHHQP.js} +1 -1
  37. package/lib/chunks/{CHX7O5AM.js → GKPSJEG5.js} +1 -1
  38. package/lib/chunks/{TN3JQHMS.js → GMMFZFKJ.js} +1 -1
  39. package/lib/chunks/{DVLD4FET.js → GW4PLLFM.js} +1 -1
  40. package/lib/chunks/{L2O63VRM.js → HIMEF7J2.js} +1 -1
  41. package/lib/chunks/{Y3LDWYZD.js → HJ5KS42K.js} +1 -1
  42. package/lib/chunks/{IZMDARCL.js → HXOTKK2E.js} +1 -1
  43. package/lib/chunks/{HBWCMTP3.js → IFGXTL5P.js} +1 -1
  44. package/lib/chunks/{NLAPR7D4.js → IHUHQ7PO.js} +1 -1
  45. package/lib/chunks/{EO65RJ4Z.js → IJYEZR53.js} +1 -1
  46. package/lib/chunks/{BOWZV5S6.js → IKSSZYHA.js} +1 -1
  47. package/lib/chunks/{QZJAXPYF.js → IMNPDC6N.js} +1 -1
  48. package/lib/chunks/{25RSTUQA.js → IO7M7MGV.js} +1 -1
  49. package/lib/chunks/{QXHFS5G6.js → J3TXBQDL.js} +1 -1
  50. package/lib/chunks/{7HFLPQAB.js → JZV7EL4R.js} +1 -1
  51. package/lib/chunks/{2NYYNBAC.js → K6HV4EJS.js} +1 -1
  52. package/lib/chunks/{WLI2XLEP.js → KVXVR5Y4.js} +33 -33
  53. package/lib/chunks/{RNA6JDRC.js → L4HVZOFP.js} +1 -1
  54. package/lib/chunks/{WCJ2HAN3.js → L5AG5NJZ.js} +1 -1
  55. package/lib/chunks/{ZBB6PS6I.js → LTJXST6M.js} +1 -1
  56. package/lib/chunks/MO6NEDYO.js +17 -0
  57. package/lib/chunks/{S6W63XAS.js → NDECHUWV.js} +1 -1
  58. package/lib/chunks/{XZIK3H6V.js → NFGCHP5P.js} +1 -1
  59. package/lib/chunks/{W2CABGMW.js → NNBSSORJ.js} +1 -1
  60. package/lib/chunks/{XGPGFZMH.js → OLAN2CVV.js} +1 -1
  61. package/lib/chunks/{TZROZMVE.js → OOKU5T7P.js} +7 -7
  62. package/lib/chunks/{DURXFJ7H.js → OQRUWVII.js} +1 -1
  63. package/lib/chunks/{HWOL3HHR.js → Q5WCA45H.js} +8 -8
  64. package/lib/chunks/{HAGYQ4BK.js → QGKTPGO7.js} +1 -1
  65. package/lib/chunks/{UMN7TKTH.js → QQ2PGWE6.js} +2 -2
  66. package/lib/chunks/QYBWIG5Y.js +59 -0
  67. package/lib/chunks/{CJWIDJTB.js → RC4LK3ZS.js} +2 -2
  68. package/lib/chunks/{O53IFFP3.js → RL2EIWFM.js} +1 -1
  69. package/lib/chunks/{5BQNATJZ.js → RQMFL6Q4.js} +1 -1
  70. package/lib/chunks/S3B562IT.js +7 -0
  71. package/lib/chunks/{EQOFXQKD.js → SPIFPTTE.js} +1 -1
  72. package/lib/chunks/SUQ7ZWYJ.js +7 -0
  73. package/lib/chunks/{BE4Z5FOC.js → TFPMT2XD.js} +1603 -16
  74. package/lib/chunks/{AH6ANNYP.js → TVUYXOSQ.js} +1 -1
  75. package/lib/chunks/{ACECCMWJ.js → UFDBFVRI.js} +1 -1
  76. package/lib/chunks/UH4Q6U6X.js +7 -0
  77. package/lib/chunks/{YL3LVBZF.js → UKVHQIBO.js} +1 -1
  78. package/lib/chunks/{4DUOPCYP.js → VF2YOZ3U.js} +1 -1
  79. package/lib/chunks/{6ZNJ7RDX.js → VO775J6V.js} +2 -2
  80. package/lib/chunks/WKBXTA7Y.js +53 -0
  81. package/lib/chunks/{RUPTJCF5.js → WRGAJXOK.js} +1 -1
  82. package/lib/chunks/{EBHQXW6Z.js → XKENZH7W.js} +1 -1
  83. package/lib/chunks/{B5SUTVG4.js → YB52VXMW.js} +1 -1
  84. package/lib/chunks/{G6SB4ZP5.js → ZSQF6YOF.js} +1 -1
  85. package/lib/chunks/{54EKLNNI.js → ZYHLWT2C.js} +1 -1
  86. package/lib/eject-assets/AGENTS.md +1 -1
  87. package/lib/eject-assets/generators/cli/docs.ts +1 -1
  88. package/lib/eject-assets/generators/cli/engine-source.ts +1 -1
  89. package/lib/eject-assets/generators/cli/index.ts +2 -2
  90. package/lib/eject-assets/generators/cli/render.ts +1 -1
  91. package/lib/eject-assets/generators/go/client.ts +1 -1
  92. package/lib/eject-assets/generators/go/descriptor.ts +1 -1
  93. package/lib/eject-assets/generators/go/index.ts +2 -2
  94. package/lib/eject-assets/generators/go/models.ts +1 -1
  95. package/lib/eject-assets/generators/go/naming.ts +1 -1
  96. package/lib/eject-assets/generators/go/operations.ts +1 -1
  97. package/lib/eject-assets/generators/go/pagination.ts +1 -1
  98. package/lib/eject-assets/generators/go/types.ts +1 -1
  99. package/lib/eject-assets/generators/java/client.ts +66 -0
  100. package/lib/eject-assets/generators/java/descriptor.ts +72 -0
  101. package/lib/eject-assets/generators/java/index.ts +245 -0
  102. package/lib/eject-assets/generators/java/models.ts +362 -0
  103. package/lib/eject-assets/generators/java/naming.ts +135 -0
  104. package/lib/eject-assets/generators/java/operations.ts +300 -0
  105. package/lib/eject-assets/generators/java/pagination.ts +94 -0
  106. package/lib/eject-assets/generators/java/types.ts +103 -0
  107. package/lib/eject-assets/generators/mock/faker.ts +1 -1
  108. package/lib/eject-assets/generators/mock/index.ts +2 -2
  109. package/lib/eject-assets/generators/mock/render.ts +1 -1
  110. package/lib/eject-assets/generators/mock/sample.ts +1 -1
  111. package/lib/eject-assets/generators/mock/values.ts +1 -1
  112. package/lib/eject-assets/generators/php/client.ts +1 -1
  113. package/lib/eject-assets/generators/php/descriptor.ts +1 -1
  114. package/lib/eject-assets/generators/php/index.ts +2 -2
  115. package/lib/eject-assets/generators/php/models.ts +1 -1
  116. package/lib/eject-assets/generators/php/naming.ts +1 -1
  117. package/lib/eject-assets/generators/php/operations.ts +1 -1
  118. package/lib/eject-assets/generators/php/pagination.ts +1 -1
  119. package/lib/eject-assets/generators/php/types.ts +1 -1
  120. package/lib/eject-assets/generators/python/client.ts +1 -1
  121. package/lib/eject-assets/generators/python/descriptor.ts +1 -1
  122. package/lib/eject-assets/generators/python/index.ts +2 -2
  123. package/lib/eject-assets/generators/python/models.ts +1 -1
  124. package/lib/eject-assets/generators/python/naming.ts +1 -1
  125. package/lib/eject-assets/generators/python/operations.ts +1 -1
  126. package/lib/eject-assets/generators/python/pagination.ts +1 -1
  127. package/lib/eject-assets/generators/python/types.ts +1 -1
  128. package/lib/eject-assets/generators/swr/index.ts +2 -2
  129. package/lib/eject-assets/generators/swr/render.ts +1 -1
  130. package/lib/eject-assets/generators/tanstack-query/index.ts +2 -2
  131. package/lib/eject-assets/generators/tanstack-query/render.ts +1 -1
  132. package/lib/eject-assets/generators/transformers/index.ts +2 -2
  133. package/lib/eject-assets/generators/transformers/render.ts +1 -1
  134. package/lib/eject-assets/generators/typescript/banner.ts +1 -1
  135. package/lib/eject-assets/generators/typescript/client.ts +1 -1
  136. package/lib/eject-assets/generators/typescript/descriptor.ts +1 -1
  137. package/lib/eject-assets/generators/typescript/index.ts +2 -2
  138. package/lib/eject-assets/generators/typescript/inline-runtime.ts +1 -1
  139. package/lib/eject-assets/generators/typescript/operation-signature.ts +1 -1
  140. package/lib/eject-assets/generators/typescript/operation-types.ts +1 -1
  141. package/lib/eject-assets/generators/typescript/operations.ts +1 -1
  142. package/lib/eject-assets/generators/typescript/response-headers.ts +1 -1
  143. package/lib/eject-assets/generators/typescript/type-guards.ts +1 -1
  144. package/lib/eject-assets/generators/typescript/types.ts +1 -1
  145. package/lib/eject-assets/generators/zod/index.ts +2 -2
  146. package/lib/eject-assets/generators/zod/schemas.ts +1 -1
  147. package/lib/eject-assets/skills/client-generators/SKILL.md +1 -1
  148. package/lib/eject-assets/skills/java-generator/SKILL.md +161 -0
  149. package/lib/index.js +91 -90
  150. package/lib/skills/redocly-cli/SKILL.md +3 -1
  151. package/package.json +1 -1
  152. package/lib/chunks/DORXGE7Z.js +0 -199
  153. package/lib/chunks/EAFZ6DNQ.js +0 -53
  154. package/lib/chunks/FGJDZKD2.js +0 -13
  155. package/lib/chunks/GAKWND4S.js +0 -8
  156. package/lib/chunks/JOWPA3MJ.js +0 -7
  157. package/lib/chunks/RBJCAITC.js +0 -9
  158. package/lib/chunks/RNL7TC2W.js +0 -17
  159. package/lib/chunks/U5DIFWGH.js +0 -7
  160. package/lib/chunks/U7KIB7CD.js +0 -7
  161. package/lib/chunks/V34D5CUU.js +0 -7
@@ -917,7 +917,1532 @@ func rawBody(body any) (io.Reader, bool, bool) {
917
917
  }
918
918
  return nil, false, false
919
919
  }
920
- `;var t=`<?php
920
+ `;var t=`// @redocly/client-generator Java runtime \u2014 embedded into generated clients.
921
+ // Java >= 17, zero dependencies; HTTP over java.net.http.HttpClient, JSON over the
922
+ // small reader/writer below. Hand-authored once and stitched into every generated
923
+ // client (see scripts/generate-runtime-sources.mjs): the generated class inlines this
924
+ // class body, or extends this class under \`--runtime module\`, so every name below is
925
+ // reachable unqualified either way. Semantically in lockstep with the TypeScript
926
+ // runtime: auth OR-alternatives, a retry loop with Retry-After and full-jitter backoff,
927
+ // per-attempt timeouts, idempotency keys, middleware hooks, pagination, and SSE.
928
+
929
+ import java.io.ByteArrayOutputStream;
930
+ import java.io.IOException;
931
+ import java.io.InputStream;
932
+ import java.io.UncheckedIOException;
933
+ import java.net.URI;
934
+ import java.net.URLEncoder;
935
+ import java.net.http.HttpClient;
936
+ import java.net.http.HttpRequest;
937
+ import java.net.http.HttpResponse;
938
+ import java.net.http.HttpTimeoutException;
939
+ import java.nio.charset.StandardCharsets;
940
+ import java.time.Duration;
941
+ import java.time.LocalDate;
942
+ import java.time.OffsetDateTime;
943
+ import java.time.format.DateTimeFormatter;
944
+ import java.util.ArrayList;
945
+ import java.util.Base64;
946
+ import java.util.Collections;
947
+ import java.util.Iterator;
948
+ import java.util.LinkedHashMap;
949
+ import java.util.List;
950
+ import java.util.Locale;
951
+ import java.util.Map;
952
+ import java.util.NoSuchElementException;
953
+ import java.util.Objects;
954
+ import java.util.concurrent.ThreadLocalRandom;
955
+ import java.util.function.Function;
956
+ import java.util.function.Supplier;
957
+
958
+ abstract class Runtime {
959
+ // \u2500\u2500\u2500 Errors \u2500\u2500\u2500
960
+
961
+ /** A response with status >= 400, with the decoded body attached. */
962
+ public static class ApiException extends RuntimeException {
963
+ private static final long serialVersionUID = 1L;
964
+ public final String url;
965
+ public final int status;
966
+ public final transient Object body;
967
+
968
+ public ApiException(String url, int status, Object body) {
969
+ super("Request failed with status " + status + " for " + url);
970
+ this.url = url;
971
+ this.status = status;
972
+ this.body = body;
973
+ }
974
+ }
975
+
976
+ /** A request attempt exceeded the configured timeout. */
977
+ public static class ApiTimeoutException extends RuntimeException {
978
+ private static final long serialVersionUID = 1L;
979
+ public final String operationId;
980
+ public final Duration timeout;
981
+ public final int attempt;
982
+
983
+ public ApiTimeoutException(String operationId, Duration timeout, int attempt) {
984
+ super("Request \\"" + operationId + "\\" timed out after " + timeout + " (attempt " + attempt + ")");
985
+ this.operationId = operationId;
986
+ this.timeout = timeout;
987
+ this.attempt = attempt;
988
+ }
989
+ }
990
+
991
+ // \u2500\u2500\u2500 Configuration \u2500\u2500\u2500
992
+
993
+ public record BasicAuth(String username, String password) {}
994
+
995
+ /** The client credentials; an empty Auth is anonymous. Providers are called per request. */
996
+ public static final class Auth {
997
+ Supplier<String> bearer;
998
+ BasicAuth basic;
999
+ final Map<String, Supplier<String>> apiKey = new LinkedHashMap<>();
1000
+
1001
+ public Auth bearer(String token) {
1002
+ return bearer(() -> token);
1003
+ }
1004
+
1005
+ public Auth bearer(Supplier<String> provider) {
1006
+ this.bearer = provider;
1007
+ return this;
1008
+ }
1009
+
1010
+ public Auth basic(String username, String password) {
1011
+ this.basic = new BasicAuth(username, password);
1012
+ return this;
1013
+ }
1014
+
1015
+ public Auth apiKey(String scheme, String value) {
1016
+ return apiKey(scheme, () -> value);
1017
+ }
1018
+
1019
+ public Auth apiKey(String scheme, Supplier<String> provider) {
1020
+ this.apiKey.put(scheme, provider);
1021
+ return this;
1022
+ }
1023
+ }
1024
+
1025
+ /** Decides whether an attempt is retried: the response, or the error when there is none. */
1026
+ @FunctionalInterface
1027
+ public interface RetryOn {
1028
+ boolean test(int attempt, HttpResponse<?> response, Exception error);
1029
+ }
1030
+
1031
+ /** The retry policy: \`retries\` extra attempts, with fixed or exponential (default) backoff. */
1032
+ public static final class Retry {
1033
+ int retries;
1034
+ Duration delay = Duration.ofSeconds(1);
1035
+ String strategy = "exponential";
1036
+ boolean noJitter;
1037
+ RetryOn retryOn;
1038
+
1039
+ public Retry retries(int retries) {
1040
+ this.retries = retries;
1041
+ return this;
1042
+ }
1043
+
1044
+ public Retry delay(Duration delay) {
1045
+ this.delay = delay;
1046
+ return this;
1047
+ }
1048
+
1049
+ /** \`"exponential"\` (default) or \`"fixed"\`. */
1050
+ public Retry strategy(String strategy) {
1051
+ this.strategy = strategy;
1052
+ return this;
1053
+ }
1054
+
1055
+ public Retry noJitter(boolean noJitter) {
1056
+ this.noJitter = noJitter;
1057
+ return this;
1058
+ }
1059
+
1060
+ /** Replaces the default predicate (idempotent methods on 408/429/5xx or a transport error). */
1061
+ public Retry retryOn(RetryOn retryOn) {
1062
+ this.retryOn = retryOn;
1063
+ return this;
1064
+ }
1065
+ }
1066
+
1067
+ /** Hooks around every attempt: \`onRequest\` before it is sent, \`onResponse\` in reverse order. */
1068
+ public interface Middleware {
1069
+ default void onRequest(HttpRequest.Builder request) {}
1070
+
1071
+ default void onResponse(HttpResponse<InputStream> response) {}
1072
+ }
1073
+
1074
+ /** Per-client configuration shared by every operation method. */
1075
+ public static final class Config {
1076
+ String serverUrl = "";
1077
+ HttpClient httpClient;
1078
+ final Map<String, String> headers = new LinkedHashMap<>();
1079
+ Duration timeout;
1080
+ Retry retry = new Retry();
1081
+ final List<Middleware> middleware = new ArrayList<>();
1082
+ Supplier<String> idempotencyKey;
1083
+ Auth auth = new Auth();
1084
+ String clientHeader = "redocly-client-generator";
1085
+
1086
+ public Config serverUrl(String serverUrl) {
1087
+ this.serverUrl = serverUrl;
1088
+ return this;
1089
+ }
1090
+
1091
+ public Config httpClient(HttpClient httpClient) {
1092
+ this.httpClient = httpClient;
1093
+ return this;
1094
+ }
1095
+
1096
+ /** A header sent with every request; a \`Content-Type\` here wins over the description's. */
1097
+ public Config header(String name, String value) {
1098
+ this.headers.put(name, value);
1099
+ return this;
1100
+ }
1101
+
1102
+ /** The budget of one attempt; a later attempt gets a fresh one. */
1103
+ public Config timeout(Duration timeout) {
1104
+ this.timeout = timeout;
1105
+ return this;
1106
+ }
1107
+
1108
+ public Config retry(Retry retry) {
1109
+ this.retry = retry;
1110
+ return this;
1111
+ }
1112
+
1113
+ public Config middleware(Middleware middleware) {
1114
+ this.middleware.add(middleware);
1115
+ return this;
1116
+ }
1117
+
1118
+ /** Supplies the \`Idempotency-Key\` of every POST and PATCH that does not carry one. */
1119
+ public Config idempotencyKey(Supplier<String> idempotencyKey) {
1120
+ this.idempotencyKey = idempotencyKey;
1121
+ return this;
1122
+ }
1123
+
1124
+ public Config auth(Auth auth) {
1125
+ this.auth = auth;
1126
+ return this;
1127
+ }
1128
+ }
1129
+
1130
+ /** A \`<op>WithHeaders\` result: the decoded body plus the coerced declared headers. */
1131
+ public record Envelope<T>(T data, Map<String, Object> headers, int status) {}
1132
+
1133
+ /** One decoded \`text/event-stream\` frame; \`data\` is parsed JSON for JSON event streams. */
1134
+ public record ServerSentEvent(String event, Object data, String id, Integer retry) {}
1135
+
1136
+ /** One entry of the operations table's security requirements. */
1137
+ public record SecuritySpec(String scheme, String kind, String name, String in) {
1138
+ static SecuritySpec of(String scheme, String kind) {
1139
+ return new SecuritySpec(scheme, kind, null, null);
1140
+ }
1141
+ }
1142
+
1143
+ /** One entry of the operations table's pagination rules. */
1144
+ public record PaginationSpec(
1145
+ String style,
1146
+ String param,
1147
+ String nextCursor,
1148
+ String hasMore,
1149
+ String nextLink,
1150
+ String limitParam,
1151
+ String items
1152
+ ) {}
1153
+
1154
+ record Operation(
1155
+ String id,
1156
+ String method,
1157
+ String path,
1158
+ List<List<SecuritySpec>> security,
1159
+ PaginationSpec pagination
1160
+ ) {}
1161
+
1162
+ // \u2500\u2500\u2500 JSON \u2500\u2500\u2500
1163
+
1164
+ /**
1165
+ * A minimal JSON reader and writer over plain values: \`Map<String, Object>\` (insertion
1166
+ * ordered), \`List<Object>\`, \`String\`, \`Long\` for integral numbers, \`Double\` for the rest,
1167
+ * \`Boolean\`, and \`null\`. No streaming: a response body is read whole, then parsed.
1168
+ */
1169
+ public static final class Json {
1170
+ private final String text;
1171
+ private int position;
1172
+
1173
+ private Json(String text) {
1174
+ this.text = text;
1175
+ }
1176
+
1177
+ public static Object parse(String text) {
1178
+ Json json = new Json(text);
1179
+ json.skipWhitespace();
1180
+ Object value = json.readValue();
1181
+ json.skipWhitespace();
1182
+ if (json.position != text.length()) {
1183
+ throw json.error("unexpected trailing characters");
1184
+ }
1185
+ return value;
1186
+ }
1187
+
1188
+ public static String write(Object value) {
1189
+ StringBuilder out = new StringBuilder();
1190
+ writeValue(out, value);
1191
+ return out.toString();
1192
+ }
1193
+
1194
+ private IllegalArgumentException error(String message) {
1195
+ return new IllegalArgumentException("Invalid JSON at offset " + position + ": " + message);
1196
+ }
1197
+
1198
+ private void skipWhitespace() {
1199
+ while (position < text.length()) {
1200
+ char current = text.charAt(position);
1201
+ if (current != ' ' && current != '\\n' && current != '\\r' && current != '\\t') {
1202
+ break;
1203
+ }
1204
+ position++;
1205
+ }
1206
+ }
1207
+
1208
+ private Object readValue() {
1209
+ if (position >= text.length()) {
1210
+ throw error("unexpected end of input");
1211
+ }
1212
+ char current = text.charAt(position);
1213
+ switch (current) {
1214
+ case '{':
1215
+ return readObject();
1216
+ case '[':
1217
+ return readArray();
1218
+ case '"':
1219
+ return readString();
1220
+ case 't':
1221
+ expectWord("true");
1222
+ return Boolean.TRUE;
1223
+ case 'f':
1224
+ expectWord("false");
1225
+ return Boolean.FALSE;
1226
+ case 'n':
1227
+ expectWord("null");
1228
+ return null;
1229
+ default:
1230
+ if (current == '-' || (current >= '0' && current <= '9')) {
1231
+ return readNumber();
1232
+ }
1233
+ throw error("unexpected character '" + current + "'");
1234
+ }
1235
+ }
1236
+
1237
+ private void expectWord(String word) {
1238
+ if (!text.startsWith(word, position)) {
1239
+ throw error("expected " + word);
1240
+ }
1241
+ position += word.length();
1242
+ }
1243
+
1244
+ private Map<String, Object> readObject() {
1245
+ Map<String, Object> object = new LinkedHashMap<>();
1246
+ position++;
1247
+ skipWhitespace();
1248
+ if (peek() == '}') {
1249
+ position++;
1250
+ return object;
1251
+ }
1252
+ while (true) {
1253
+ skipWhitespace();
1254
+ if (peek() != '"') {
1255
+ throw error("expected a string key");
1256
+ }
1257
+ String key = readString();
1258
+ skipWhitespace();
1259
+ if (peek() != ':') {
1260
+ throw error("expected ':'");
1261
+ }
1262
+ position++;
1263
+ skipWhitespace();
1264
+ object.put(key, readValue());
1265
+ skipWhitespace();
1266
+ char next = peek();
1267
+ position++;
1268
+ if (next == '}') {
1269
+ return object;
1270
+ }
1271
+ if (next != ',') {
1272
+ throw error("expected ',' or '}'");
1273
+ }
1274
+ }
1275
+ }
1276
+
1277
+ private List<Object> readArray() {
1278
+ List<Object> array = new ArrayList<>();
1279
+ position++;
1280
+ skipWhitespace();
1281
+ if (peek() == ']') {
1282
+ position++;
1283
+ return array;
1284
+ }
1285
+ while (true) {
1286
+ skipWhitespace();
1287
+ array.add(readValue());
1288
+ skipWhitespace();
1289
+ char next = peek();
1290
+ position++;
1291
+ if (next == ']') {
1292
+ return array;
1293
+ }
1294
+ if (next != ',') {
1295
+ throw error("expected ',' or ']'");
1296
+ }
1297
+ }
1298
+ }
1299
+
1300
+ private char peek() {
1301
+ if (position >= text.length()) {
1302
+ throw error("unexpected end of input");
1303
+ }
1304
+ return text.charAt(position);
1305
+ }
1306
+
1307
+ private String readString() {
1308
+ position++;
1309
+ StringBuilder out = new StringBuilder();
1310
+ while (true) {
1311
+ char current = peek();
1312
+ position++;
1313
+ if (current == '"') {
1314
+ return out.toString();
1315
+ }
1316
+ if (current != '\\\\') {
1317
+ out.append(current);
1318
+ continue;
1319
+ }
1320
+ char escaped = peek();
1321
+ position++;
1322
+ switch (escaped) {
1323
+ case '"':
1324
+ case '\\\\':
1325
+ case '/':
1326
+ out.append(escaped);
1327
+ break;
1328
+ case 'b':
1329
+ out.append('\\b');
1330
+ break;
1331
+ case 'f':
1332
+ out.append('\\f');
1333
+ break;
1334
+ case 'n':
1335
+ out.append('\\n');
1336
+ break;
1337
+ case 'r':
1338
+ out.append('\\r');
1339
+ break;
1340
+ case 't':
1341
+ out.append('\\t');
1342
+ break;
1343
+ case 'u':
1344
+ if (position + 4 > text.length()) {
1345
+ throw error("truncated unicode escape");
1346
+ }
1347
+ out.append((char) Integer.parseInt(text.substring(position, position + 4), 16));
1348
+ position += 4;
1349
+ break;
1350
+ default:
1351
+ throw error("invalid escape '\\\\" + escaped + "'");
1352
+ }
1353
+ }
1354
+ }
1355
+
1356
+ private Object readNumber() {
1357
+ int start = position;
1358
+ boolean integral = true;
1359
+ if (peek() == '-') {
1360
+ position++;
1361
+ }
1362
+ while (position < text.length()) {
1363
+ char current = text.charAt(position);
1364
+ if (current >= '0' && current <= '9') {
1365
+ position++;
1366
+ } else if (current == '.' || current == 'e' || current == 'E' || current == '+' || current == '-') {
1367
+ integral = false;
1368
+ position++;
1369
+ } else {
1370
+ break;
1371
+ }
1372
+ }
1373
+ String raw = text.substring(start, position);
1374
+ try {
1375
+ if (integral) {
1376
+ return Long.parseLong(raw);
1377
+ }
1378
+ return Double.parseDouble(raw);
1379
+ } catch (NumberFormatException exception) {
1380
+ // Beyond a long: keep the value rather than fail the whole document.
1381
+ return Double.parseDouble(raw);
1382
+ }
1383
+ }
1384
+
1385
+ private static void writeValue(StringBuilder out, Object value) {
1386
+ if (value == null) {
1387
+ out.append("null");
1388
+ } else if (value instanceof String string) {
1389
+ writeString(out, string);
1390
+ } else if (value instanceof Boolean || value instanceof Integer || value instanceof Long) {
1391
+ out.append(value);
1392
+ } else if (value instanceof Number number) {
1393
+ double asDouble = number.doubleValue();
1394
+ if (Double.isNaN(asDouble) || Double.isInfinite(asDouble)) {
1395
+ out.append("null");
1396
+ } else if (asDouble == Math.rint(asDouble) && Math.abs(asDouble) < 1e15) {
1397
+ out.append((long) asDouble);
1398
+ } else {
1399
+ out.append(asDouble);
1400
+ }
1401
+ } else if (value instanceof Map<?, ?> map) {
1402
+ out.append('{');
1403
+ boolean first = true;
1404
+ for (Map.Entry<?, ?> entry : map.entrySet()) {
1405
+ if (!first) {
1406
+ out.append(',');
1407
+ }
1408
+ first = false;
1409
+ writeString(out, String.valueOf(entry.getKey()));
1410
+ out.append(':');
1411
+ writeValue(out, entry.getValue());
1412
+ }
1413
+ out.append('}');
1414
+ } else if (value instanceof Iterable<?> items) {
1415
+ out.append('[');
1416
+ boolean first = true;
1417
+ for (Object item : items) {
1418
+ if (!first) {
1419
+ out.append(',');
1420
+ }
1421
+ first = false;
1422
+ writeValue(out, item);
1423
+ }
1424
+ out.append(']');
1425
+ } else if (value instanceof JsonValue typed) {
1426
+ writeValue(out, typed.toJson());
1427
+ } else if (value instanceof WireValue wire) {
1428
+ writeValue(out, wire.wire());
1429
+ } else {
1430
+ // Dates and anything else print their ISO-8601 / text form.
1431
+ writeString(out, textValue(value));
1432
+ }
1433
+ }
1434
+
1435
+ private static void writeString(StringBuilder out, String value) {
1436
+ out.append('"');
1437
+ for (int index = 0; index < value.length(); index++) {
1438
+ char current = value.charAt(index);
1439
+ switch (current) {
1440
+ case '"':
1441
+ out.append("\\\\\\"");
1442
+ break;
1443
+ case '\\\\':
1444
+ out.append("\\\\\\\\");
1445
+ break;
1446
+ case '\\n':
1447
+ out.append("\\\\n");
1448
+ break;
1449
+ case '\\r':
1450
+ out.append("\\\\r");
1451
+ break;
1452
+ case '\\t':
1453
+ out.append("\\\\t");
1454
+ break;
1455
+ default:
1456
+ if (current < 0x20) {
1457
+ out.append(String.format("\\\\u%04x", (int) current));
1458
+ } else {
1459
+ out.append(current);
1460
+ }
1461
+ }
1462
+ }
1463
+ out.append('"');
1464
+ }
1465
+ }
1466
+
1467
+ /** A generated model: serializes itself to the plain JSON graph. */
1468
+ public interface JsonValue {
1469
+ Object toJson();
1470
+ }
1471
+
1472
+ /** A generated enum: carries the wire literal (a String or a Long) of each member. */
1473
+ public interface WireValue {
1474
+ Object wire();
1475
+ }
1476
+
1477
+ // \u2500\u2500\u2500 Hydration helpers (wire value \u2192 typed value, null-tolerant) \u2500\u2500\u2500
1478
+
1479
+ static String asString(Object value) {
1480
+ return value == null ? null : value instanceof String string ? string : String.valueOf(value);
1481
+ }
1482
+
1483
+ static Long asLong(Object value) {
1484
+ if (value instanceof Number number) {
1485
+ return number.longValue();
1486
+ }
1487
+ return value instanceof String string && !string.isEmpty() ? Long.parseLong(string) : null;
1488
+ }
1489
+
1490
+ static Double asDouble(Object value) {
1491
+ if (value instanceof Number number) {
1492
+ return number.doubleValue();
1493
+ }
1494
+ return value instanceof String string && !string.isEmpty() ? Double.parseDouble(string) : null;
1495
+ }
1496
+
1497
+ static Boolean asBoolean(Object value) {
1498
+ if (value instanceof Boolean bool) {
1499
+ return bool;
1500
+ }
1501
+ return value instanceof String string ? Boolean.parseBoolean(string) : null;
1502
+ }
1503
+
1504
+ static OffsetDateTime asOffsetDateTime(Object value) {
1505
+ return value == null ? null : OffsetDateTime.parse(asString(value));
1506
+ }
1507
+
1508
+ static LocalDate asLocalDate(Object value) {
1509
+ return value == null ? null : LocalDate.parse(asString(value));
1510
+ }
1511
+
1512
+ static <T> List<T> asList(Object value, Function<Object, T> item) {
1513
+ if (!(value instanceof List<?> raw)) {
1514
+ return null;
1515
+ }
1516
+ List<T> out = new ArrayList<>(raw.size());
1517
+ for (Object entry : raw) {
1518
+ out.add(item.apply(entry));
1519
+ }
1520
+ return out;
1521
+ }
1522
+
1523
+ static <T> Map<String, T> asMap(Object value, Function<Object, T> item) {
1524
+ if (!(value instanceof Map<?, ?> raw)) {
1525
+ return null;
1526
+ }
1527
+ Map<String, T> out = new LinkedHashMap<>();
1528
+ for (Map.Entry<?, ?> entry : raw.entrySet()) {
1529
+ out.put(String.valueOf(entry.getKey()), item.apply(entry.getValue()));
1530
+ }
1531
+ return out;
1532
+ }
1533
+
1534
+ static Map<String, Object> asObject(Object value) {
1535
+ return asMap(value, item -> item);
1536
+ }
1537
+
1538
+ static <T> List<Object> mapList(List<T> value, Function<T, Object> item) {
1539
+ if (value == null) {
1540
+ return null;
1541
+ }
1542
+ List<Object> out = new ArrayList<>(value.size());
1543
+ for (T entry : value) {
1544
+ out.add(entry == null ? null : item.apply(entry));
1545
+ }
1546
+ return out;
1547
+ }
1548
+
1549
+ static <T> Map<String, Object> mapValues(Map<String, T> value, Function<T, Object> item) {
1550
+ if (value == null) {
1551
+ return null;
1552
+ }
1553
+ Map<String, Object> out = new LinkedHashMap<>();
1554
+ for (Map.Entry<String, T> entry : value.entrySet()) {
1555
+ out.put(entry.getKey(), entry.getValue() == null ? null : item.apply(entry.getValue()));
1556
+ }
1557
+ return out;
1558
+ }
1559
+
1560
+ /** The wire text of a typed value: enums by wire literal, date-times with seconds, records as JSON, the rest as text. */
1561
+ static String textValue(Object value) {
1562
+ if (value instanceof WireValue wire) {
1563
+ return String.valueOf(wire.wire());
1564
+ }
1565
+ if (value instanceof JsonValue || value instanceof Map || value instanceof Iterable) {
1566
+ return Json.write(value);
1567
+ }
1568
+ if (value instanceof OffsetDateTime dateTime) {
1569
+ return dateTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
1570
+ }
1571
+ return String.valueOf(value);
1572
+ }
1573
+
1574
+ static void addQuery(Map<String, List<String>> query, String name, Object value) {
1575
+ query.computeIfAbsent(name, key -> new ArrayList<>()).add(textValue(value));
1576
+ }
1577
+
1578
+ /** The source's elements, each mapped \u2014 the typed view of a raw page or item sequence. */
1579
+ static <T> Iterable<T> mapIterable(Iterable<Object> source, Function<Object, T> map) {
1580
+ return () -> {
1581
+ Iterator<Object> raw = source.iterator();
1582
+ return new Iterator<T>() {
1583
+ @Override
1584
+ public boolean hasNext() {
1585
+ return raw.hasNext();
1586
+ }
1587
+
1588
+ @Override
1589
+ public T next() {
1590
+ return map.apply(raw.next());
1591
+ }
1592
+ };
1593
+ };
1594
+ }
1595
+
1596
+ /** The items of every page, in order, read through the rule's \`items\` pointer. */
1597
+ static Iterable<Object> flattenItems(Iterable<Object> pages, String pointer) {
1598
+ return () -> {
1599
+ Iterator<Object> pageIterator = pages.iterator();
1600
+ return new Iterator<Object>() {
1601
+ private Iterator<?> items = Collections.emptyIterator();
1602
+
1603
+ @Override
1604
+ public boolean hasNext() {
1605
+ while (!items.hasNext()) {
1606
+ if (!pageIterator.hasNext()) {
1607
+ return false;
1608
+ }
1609
+ Object found = resolvePointer(pageIterator.next(), pointer);
1610
+ items = found instanceof List<?> list ? list.iterator() : Collections.emptyIterator();
1611
+ }
1612
+ return true;
1613
+ }
1614
+
1615
+ @Override
1616
+ public Object next() {
1617
+ if (!hasNext()) {
1618
+ throw new NoSuchElementException();
1619
+ }
1620
+ return items.next();
1621
+ }
1622
+ };
1623
+ };
1624
+ }
1625
+
1626
+ static Map<String, List<String>> cloneQuery(Map<String, List<String>> query) {
1627
+ Map<String, List<String>> out = new LinkedHashMap<>();
1628
+ for (Map.Entry<String, List<String>> entry : query.entrySet()) {
1629
+ out.put(entry.getKey(), new ArrayList<>(entry.getValue()));
1630
+ }
1631
+ return out;
1632
+ }
1633
+
1634
+ // \u2500\u2500\u2500 Auth and URLs \u2500\u2500\u2500
1635
+
1636
+ record ResolvedAuth(Map<String, String> headers, Map<String, List<String>> query) {}
1637
+
1638
+ private static boolean schemeConfigured(SecuritySpec spec, Auth auth) {
1639
+ switch (spec.kind()) {
1640
+ case "apiKey":
1641
+ return auth.apiKey.containsKey(spec.scheme());
1642
+ case "bearer":
1643
+ return auth.bearer != null;
1644
+ default:
1645
+ return auth.basic != null;
1646
+ }
1647
+ }
1648
+
1649
+ /**
1650
+ * Apply the first fully-configured OR-alternative; when none is, the first alternative's
1651
+ * configured schemes are still sent (the server rejects the request \u2014 same behavior as
1652
+ * the TypeScript runtime).
1653
+ */
1654
+ static ResolvedAuth resolveAuth(List<List<SecuritySpec>> security, Auth auth) {
1655
+ Map<String, String> headers = new LinkedHashMap<>();
1656
+ Map<String, List<String>> query = new LinkedHashMap<>();
1657
+ if (security == null || security.isEmpty()) {
1658
+ return new ResolvedAuth(headers, query);
1659
+ }
1660
+ List<SecuritySpec> alternative = security.get(0);
1661
+ for (List<SecuritySpec> candidate : security) {
1662
+ boolean all = true;
1663
+ for (SecuritySpec spec : candidate) {
1664
+ if (!schemeConfigured(spec, auth)) {
1665
+ all = false;
1666
+ break;
1667
+ }
1668
+ }
1669
+ if (all) {
1670
+ alternative = candidate;
1671
+ break;
1672
+ }
1673
+ }
1674
+ List<String> cookies = new ArrayList<>();
1675
+ for (SecuritySpec spec : alternative) {
1676
+ switch (spec.kind()) {
1677
+ case "apiKey": {
1678
+ Supplier<String> provider = auth.apiKey.get(spec.scheme());
1679
+ if (provider == null) {
1680
+ continue;
1681
+ }
1682
+ String value = provider.get();
1683
+ if ("query".equals(spec.in())) {
1684
+ addQuery(query, spec.name(), value);
1685
+ } else if ("cookie".equals(spec.in())) {
1686
+ cookies.add(spec.name() + "=" + encode(value));
1687
+ } else {
1688
+ headers.put(spec.name(), value);
1689
+ }
1690
+ break;
1691
+ }
1692
+ case "bearer":
1693
+ if (auth.bearer != null) {
1694
+ headers.put("Authorization", "Bearer " + auth.bearer.get());
1695
+ }
1696
+ break;
1697
+ default:
1698
+ if (auth.basic != null) {
1699
+ String token = auth.basic.username() + ":" + auth.basic.password();
1700
+ headers.put(
1701
+ "Authorization",
1702
+ "Basic " + Base64.getEncoder().encodeToString(token.getBytes(StandardCharsets.UTF_8))
1703
+ );
1704
+ }
1705
+ }
1706
+ }
1707
+ if (!cookies.isEmpty()) {
1708
+ headers.put("Cookie", String.join("; ", cookies));
1709
+ }
1710
+ return new ResolvedAuth(headers, query);
1711
+ }
1712
+
1713
+ /** Percent-encode one path segment or query component. */
1714
+ static String encode(String value) {
1715
+ return URLEncoder.encode(value, StandardCharsets.UTF_8).replace("+", "%20");
1716
+ }
1717
+
1718
+ /** Substitute \`{param}\` placeholders with encoded values and prefix the server URL. */
1719
+ static String buildUrl(String serverUrl, String path, Map<String, String> pathParams) {
1720
+ String filled = path;
1721
+ for (Map.Entry<String, String> entry : pathParams.entrySet()) {
1722
+ filled = filled.replace("{" + entry.getKey() + "}", encode(entry.getValue()));
1723
+ }
1724
+ String base = serverUrl;
1725
+ while (base.endsWith("/")) {
1726
+ base = base.substring(0, base.length() - 1);
1727
+ }
1728
+ return base + filled;
1729
+ }
1730
+
1731
+ /** Append query params in form style: a list value repeats the key (\`tag=a&tag=b\`). */
1732
+ static String appendQuery(String url, Map<String, List<String>> query) {
1733
+ StringBuilder pairs = new StringBuilder();
1734
+ for (Map.Entry<String, List<String>> entry : query.entrySet()) {
1735
+ for (String value : entry.getValue()) {
1736
+ if (pairs.length() > 0) {
1737
+ pairs.append('&');
1738
+ }
1739
+ pairs.append(encode(entry.getKey())).append('=').append(encode(value));
1740
+ }
1741
+ }
1742
+ if (pairs.length() == 0) {
1743
+ return url;
1744
+ }
1745
+ return url + (url.contains("?") ? "&" : "?") + pairs;
1746
+ }
1747
+
1748
+ // \u2500\u2500\u2500 Send \u2500\u2500\u2500
1749
+
1750
+ /** One request as the generated methods describe it; the runtime adds auth, retries, and middleware. */
1751
+ static final class RequestSpec {
1752
+ String operationId;
1753
+ String method;
1754
+ String url;
1755
+ Map<String, String> headers = new LinkedHashMap<>();
1756
+ Map<String, List<String>> query = new LinkedHashMap<>();
1757
+ /** \`byte[]\` is replayable across attempts; an \`InputStream\` is sent as is, once. */
1758
+ Object body;
1759
+ String contentType;
1760
+
1761
+ RequestSpec(String operationId, String method, String url) {
1762
+ this.operationId = operationId;
1763
+ this.method = method;
1764
+ this.url = url;
1765
+ }
1766
+
1767
+ RequestSpec headers(Map<String, String> headers) {
1768
+ this.headers = headers;
1769
+ return this;
1770
+ }
1771
+
1772
+ RequestSpec query(Map<String, List<String>> query) {
1773
+ this.query = query;
1774
+ return this;
1775
+ }
1776
+
1777
+ RequestSpec body(Object body, String contentType) {
1778
+ this.body = body;
1779
+ this.contentType = contentType;
1780
+ return this;
1781
+ }
1782
+ }
1783
+
1784
+ private static final List<Integer> TRANSIENT_STATUS = List.of(408, 429, 500, 502, 503, 504);
1785
+
1786
+ private static boolean defaultRetryOn(String method, Map<String, String> headers, HttpResponse<?> response, Exception error) {
1787
+ boolean safe = List.of("GET", "HEAD", "PUT", "DELETE", "OPTIONS").contains(method) || hasHeader(headers, "Idempotency-Key");
1788
+ if (!safe) {
1789
+ return false;
1790
+ }
1791
+ if (error != null) {
1792
+ return true;
1793
+ }
1794
+ return response != null && TRANSIENT_STATUS.contains(response.statusCode());
1795
+ }
1796
+
1797
+ private static boolean hasHeader(Map<String, String> headers, String name) {
1798
+ for (String key : headers.keySet()) {
1799
+ if (key.equalsIgnoreCase(name)) {
1800
+ return true;
1801
+ }
1802
+ }
1803
+ return false;
1804
+ }
1805
+
1806
+ private static Duration retryDelay(Retry retry, int attempt, String retryAfter) {
1807
+ if (retryAfter != null) {
1808
+ try {
1809
+ return Duration.ofMillis((long) (Double.parseDouble(retryAfter.trim()) * 1000));
1810
+ } catch (NumberFormatException ignored) {
1811
+ // An HTTP-date Retry-After falls back to the policy's own delay.
1812
+ }
1813
+ }
1814
+ Duration raw = "fixed".equals(retry.strategy) ? retry.delay : retry.delay.multipliedBy(1L << (attempt - 1));
1815
+ if (retry.noJitter) {
1816
+ return raw;
1817
+ }
1818
+ return Duration.ofMillis(ThreadLocalRandom.current().nextLong(raw.toMillis() + 1));
1819
+ }
1820
+
1821
+ private static void sleep(Duration delay) {
1822
+ try {
1823
+ Thread.sleep(Math.max(0, delay.toMillis()));
1824
+ } catch (InterruptedException interrupted) {
1825
+ Thread.currentThread().interrupt();
1826
+ throw new UncheckedIOException(new IOException("Interrupted while waiting to retry", interrupted));
1827
+ }
1828
+ }
1829
+
1830
+ /**
1831
+ * The request core: header merge, idempotency keys, the retry loop with a fresh timeout
1832
+ * per attempt, and the middleware hooks. Returns the response of the last attempt with
1833
+ * its body still unread; callers map status >= 400 to ApiException.
1834
+ */
1835
+ static HttpResponse<InputStream> send(Config config, RequestSpec spec) {
1836
+ Map<String, String> headers = new LinkedHashMap<>(config.headers);
1837
+ headers.putAll(spec.headers);
1838
+ headers.put("X-Redocly-Client", config.clientHeader);
1839
+ String method = spec.method.toUpperCase(Locale.ROOT);
1840
+ if ((method.equals("POST") || method.equals("PATCH")) && !hasHeader(headers, "Idempotency-Key") && config.idempotencyKey != null) {
1841
+ headers.put("Idempotency-Key", config.idempotencyKey.get());
1842
+ }
1843
+ if (spec.contentType != null && spec.body != null && !hasHeader(headers, "Content-Type")) {
1844
+ headers.put("Content-Type", spec.contentType);
1845
+ }
1846
+ if (config.httpClient == null) {
1847
+ // HTTP/1.1 outright: the default client's h2c upgrade attempt confuses servers that
1848
+ // do not speak HTTP/2, and a generated SDK cannot know which it will meet.
1849
+ config.httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();
1850
+ }
1851
+ HttpClient httpClient = config.httpClient;
1852
+ String fullUrl = appendQuery(spec.url, spec.query);
1853
+ boolean stream = spec.body instanceof InputStream;
1854
+ // The first attempt consumes a stream, so there is nothing to replay.
1855
+ int maxAttempts = stream ? 1 : 1 + Math.max(0, config.retry.retries);
1856
+ for (int attempt = 1; ; attempt++) {
1857
+ HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(fullUrl));
1858
+ if (config.timeout != null) {
1859
+ builder.timeout(config.timeout);
1860
+ }
1861
+ for (Map.Entry<String, String> entry : headers.entrySet()) {
1862
+ builder.header(entry.getKey(), entry.getValue());
1863
+ }
1864
+ HttpRequest.BodyPublisher publisher;
1865
+ if (spec.body instanceof InputStream input) {
1866
+ publisher = HttpRequest.BodyPublishers.ofInputStream(() -> input);
1867
+ } else if (spec.body instanceof byte[] bytes) {
1868
+ publisher = HttpRequest.BodyPublishers.ofByteArray(bytes);
1869
+ } else {
1870
+ publisher = HttpRequest.BodyPublishers.noBody();
1871
+ }
1872
+ builder.method(method, publisher);
1873
+ for (Middleware middleware : config.middleware) {
1874
+ middleware.onRequest(builder);
1875
+ }
1876
+ HttpResponse<InputStream> response = null;
1877
+ Exception failure = null;
1878
+ try {
1879
+ response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofInputStream());
1880
+ } catch (IOException | InterruptedException exception) {
1881
+ if (exception instanceof InterruptedException) {
1882
+ Thread.currentThread().interrupt();
1883
+ }
1884
+ failure = exception;
1885
+ }
1886
+ boolean retryable = config.retry.retryOn != null
1887
+ ? config.retry.retryOn.test(attempt, response, failure)
1888
+ : defaultRetryOn(method, headers, response, failure);
1889
+ if (failure != null) {
1890
+ if (attempt < maxAttempts && retryable) {
1891
+ sleep(retryDelay(config.retry, attempt, null));
1892
+ continue;
1893
+ }
1894
+ if (failure instanceof HttpTimeoutException) {
1895
+ throw new ApiTimeoutException(spec.operationId, config.timeout, attempt);
1896
+ }
1897
+ throw new UncheckedIOException("Request to " + fullUrl + " failed: " + failure.getMessage(), new IOException(failure));
1898
+ }
1899
+ for (int index = config.middleware.size() - 1; index >= 0; index--) {
1900
+ config.middleware.get(index).onResponse(response);
1901
+ }
1902
+ if (response.statusCode() >= 400 && attempt < maxAttempts && retryable) {
1903
+ String retryAfter = response.headers().firstValue("Retry-After").orElse(null);
1904
+ readBody(response);
1905
+ sleep(retryDelay(config.retry, attempt, retryAfter));
1906
+ continue;
1907
+ }
1908
+ return response;
1909
+ }
1910
+ }
1911
+
1912
+ /** The whole response body; the stream is closed afterwards. */
1913
+ static byte[] readBody(HttpResponse<InputStream> response) {
1914
+ try (InputStream body = response.body()) {
1915
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
1916
+ byte[] chunk = new byte[8192];
1917
+ int read;
1918
+ while ((read = body.read(chunk)) != -1) {
1919
+ out.write(chunk, 0, read);
1920
+ }
1921
+ return out.toByteArray();
1922
+ } catch (IOException exception) {
1923
+ throw new UncheckedIOException(exception);
1924
+ }
1925
+ }
1926
+
1927
+ /** The decoded JSON body, or null for an empty body. */
1928
+ static Object decodeJson(HttpResponse<InputStream> response) {
1929
+ String text = new String(readBody(response), StandardCharsets.UTF_8);
1930
+ return text.isBlank() ? null : Json.parse(text);
1931
+ }
1932
+
1933
+ /** The structured exception for a non-2xx response; the body is decoded when it is JSON. */
1934
+ static ApiException apiErrorFrom(HttpResponse<InputStream> response, String url) {
1935
+ String text = new String(readBody(response), StandardCharsets.UTF_8);
1936
+ Object body = text;
1937
+ if (!text.isBlank()) {
1938
+ try {
1939
+ body = Json.parse(text);
1940
+ } catch (IllegalArgumentException notJson) {
1941
+ body = text;
1942
+ }
1943
+ }
1944
+ return new ApiException(url, response.statusCode(), body);
1945
+ }
1946
+
1947
+ /** Coerce declared response headers per \`{name, key, type}\` specs; absent or unparsable ones are omitted. */
1948
+ static Map<String, Object> readEnvelopeHeaders(HttpResponse<?> response, String[][] specs) {
1949
+ Map<String, Object> headers = new LinkedHashMap<>();
1950
+ for (String[] spec : specs) {
1951
+ String raw = response.headers().firstValue(spec[0]).orElse(null);
1952
+ if (raw == null) {
1953
+ continue;
1954
+ }
1955
+ String trimmed = raw.trim();
1956
+ try {
1957
+ switch (spec[2]) {
1958
+ case "integer":
1959
+ headers.put(spec[1], Long.parseLong(trimmed));
1960
+ break;
1961
+ case "number":
1962
+ headers.put(spec[1], Double.parseDouble(trimmed));
1963
+ break;
1964
+ case "boolean": {
1965
+ String lower = trimmed.toLowerCase(Locale.ROOT);
1966
+ if (lower.equals("true") || lower.equals("false")) {
1967
+ headers.put(spec[1], lower.equals("true"));
1968
+ }
1969
+ break;
1970
+ }
1971
+ default:
1972
+ headers.put(spec[1], raw);
1973
+ }
1974
+ } catch (NumberFormatException unparsable) {
1975
+ // Omitted, like an absent header.
1976
+ }
1977
+ }
1978
+ return headers;
1979
+ }
1980
+
1981
+ // \u2500\u2500\u2500 Pagination \u2500\u2500\u2500
1982
+
1983
+ /** Walk an RFC 6901 JSON pointer over decoded JSON; null on any miss. */
1984
+ static Object resolvePointer(Object data, String pointer) {
1985
+ if (pointer == null || pointer.isEmpty()) {
1986
+ return data;
1987
+ }
1988
+ if (!pointer.startsWith("/")) {
1989
+ return null;
1990
+ }
1991
+ Object current = data;
1992
+ for (String token : pointer.substring(1).split("/", -1)) {
1993
+ String key = token.replace("~1", "/").replace("~0", "~");
1994
+ if (current instanceof Map<?, ?> map) {
1995
+ current = map.get(key);
1996
+ } else if (current instanceof List<?> list) {
1997
+ try {
1998
+ int index = Integer.parseInt(key);
1999
+ current = index >= 0 && index < list.size() ? list.get(index) : null;
2000
+ } catch (NumberFormatException notAnIndex) {
2001
+ return null;
2002
+ }
2003
+ } else {
2004
+ return null;
2005
+ }
2006
+ if (current == null) {
2007
+ return null;
2008
+ }
2009
+ }
2010
+ return current;
2011
+ }
2012
+
2013
+ /** The \`rel="next"\` target of a \`Link\` header, or null. */
2014
+ static String linkNext(String header) {
2015
+ if (header == null) {
2016
+ return null;
2017
+ }
2018
+ for (String entry : header.split(",")) {
2019
+ String[] parts = entry.split(";");
2020
+ if (parts.length < 2) {
2021
+ continue;
2022
+ }
2023
+ String target = parts[0].trim();
2024
+ if (!target.startsWith("<") || !target.endsWith(">")) {
2025
+ continue;
2026
+ }
2027
+ for (int index = 1; index < parts.length; index++) {
2028
+ String param = parts[index].trim();
2029
+ if (param.startsWith("rel=")) {
2030
+ String rel = param.substring(4).replace("\\"", "");
2031
+ for (String kind : rel.split("\\\\s+")) {
2032
+ if (kind.equals("next")) {
2033
+ return target.substring(1, target.length() - 1);
2034
+ }
2035
+ }
2036
+ }
2037
+ }
2038
+ }
2039
+ return null;
2040
+ }
2041
+
2042
+ /** One page fetch: the decoded page and the response it came from. */
2043
+ record PageResult(Object page, HttpResponse<InputStream> response) {}
2044
+
2045
+ @FunctionalInterface
2046
+ interface PageCall {
2047
+ PageResult call(Map<String, List<String>> params);
2048
+ }
2049
+
2050
+ /** An iterator that fetches its next element lazily, once per \`hasNext\`. */
2051
+ abstract static class LazyIterator<T> implements Iterator<T> {
2052
+ private T next;
2053
+ private boolean ready;
2054
+ private boolean done;
2055
+
2056
+ /** The next element, or null when the sequence has ended. */
2057
+ protected abstract T fetch();
2058
+
2059
+ @Override
2060
+ public boolean hasNext() {
2061
+ if (done) {
2062
+ return false;
2063
+ }
2064
+ if (!ready) {
2065
+ next = fetch();
2066
+ ready = true;
2067
+ if (next == null) {
2068
+ done = true;
2069
+ }
2070
+ }
2071
+ return !done;
2072
+ }
2073
+
2074
+ @Override
2075
+ public T next() {
2076
+ if (!hasNext()) {
2077
+ throw new NoSuchElementException();
2078
+ }
2079
+ ready = false;
2080
+ return next;
2081
+ }
2082
+ }
2083
+
2084
+ /**
2085
+ * Auto-pagination over raw decoded pages, following the rule in \`spec\` \u2014 the same stop
2086
+ * conditions and loop guards as the TypeScript runtime. Generated wrappers hydrate the
2087
+ * pages into models.
2088
+ */
2089
+ static Iterable<Object> iterPages(PageCall call, PaginationSpec spec, Map<String, List<String>> base) {
2090
+ return () -> new LazyIterator<>() {
2091
+ private boolean started;
2092
+ private boolean finished;
2093
+ private Object cursor;
2094
+ private Map<String, List<String>> params = cloneQuery(base);
2095
+ private String previousLink;
2096
+ private String previousItems;
2097
+ private int position;
2098
+
2099
+ @Override
2100
+ protected Object fetch() {
2101
+ if (finished) {
2102
+ return null;
2103
+ }
2104
+ switch (spec.style()) {
2105
+ case "cursor":
2106
+ return fetchCursor();
2107
+ case "link":
2108
+ case "body-link":
2109
+ return fetchLink();
2110
+ default:
2111
+ return fetchOffset();
2112
+ }
2113
+ }
2114
+
2115
+ private Object fetchCursor() {
2116
+ if (!started) {
2117
+ started = true;
2118
+ List<String> seed = base.get(spec.param());
2119
+ cursor = seed == null || seed.isEmpty() ? null : seed.get(0);
2120
+ }
2121
+ Map<String, List<String>> pageParams = cloneQuery(base);
2122
+ if (cursor != null) {
2123
+ pageParams.put(spec.param(), new ArrayList<>(List.of(String.valueOf(cursor))));
2124
+ }
2125
+ Object page = call.call(pageParams).page();
2126
+ if (spec.hasMore() != null && Boolean.FALSE.equals(resolvePointer(page, spec.hasMore()))) {
2127
+ finished = true;
2128
+ return page;
2129
+ }
2130
+ Object next = resolvePointer(page, spec.nextCursor());
2131
+ if (next == null || "".equals(next)) {
2132
+ finished = true;
2133
+ return page;
2134
+ }
2135
+ if (!(next instanceof String) && !(next instanceof Number)) {
2136
+ throw new IllegalStateException("Pagination cursor at " + spec.nextCursor() + " is not a string or number");
2137
+ }
2138
+ if (cursor != null && String.valueOf(next).equals(String.valueOf(cursor))) {
2139
+ throw new IllegalStateException("Pagination did not advance: the operation returned the same cursor twice");
2140
+ }
2141
+ cursor = next;
2142
+ return page;
2143
+ }
2144
+
2145
+ private Object fetchLink() {
2146
+ PageResult result = call.call(params);
2147
+ String target;
2148
+ if (spec.style().equals("link")) {
2149
+ target = linkNext(result.response().headers().firstValue("Link").orElse(null));
2150
+ } else {
2151
+ Object value = resolvePointer(result.page(), spec.nextLink());
2152
+ if (value != null && !(value instanceof String)) {
2153
+ throw new IllegalStateException("Pagination link at " + spec.nextLink() + " is not a string");
2154
+ }
2155
+ target = (String) value;
2156
+ }
2157
+ if (target == null || target.isEmpty()) {
2158
+ finished = true;
2159
+ return result.page();
2160
+ }
2161
+ URI pageUri = result.response().uri();
2162
+ URI next = pageUri.resolve(target);
2163
+ if (next.toString().equals(previousLink) || next.toString().equals(pageUri.toString())) {
2164
+ throw new IllegalStateException("Pagination did not advance: the Link rel=\\"next\\" target repeats");
2165
+ }
2166
+ previousLink = next.toString();
2167
+ params = cloneQuery(base);
2168
+ String rawQuery = next.getRawQuery();
2169
+ if (rawQuery != null) {
2170
+ for (String pair : rawQuery.split("&")) {
2171
+ if (pair.isEmpty()) {
2172
+ continue;
2173
+ }
2174
+ int equals = pair.indexOf('=');
2175
+ String key = java.net.URLDecoder.decode(equals < 0 ? pair : pair.substring(0, equals), StandardCharsets.UTF_8);
2176
+ String value = equals < 0 ? "" : java.net.URLDecoder.decode(pair.substring(equals + 1), StandardCharsets.UTF_8);
2177
+ params.put(key, new ArrayList<>(List.of(value)));
2178
+ }
2179
+ }
2180
+ return result.page();
2181
+ }
2182
+
2183
+ private Object fetchOffset() {
2184
+ if (!started) {
2185
+ started = true;
2186
+ position = spec.style().equals("page") ? 1 : 0;
2187
+ List<String> seed = base.get(spec.param());
2188
+ if (seed != null && !seed.isEmpty() && !seed.get(0).isEmpty()) {
2189
+ try {
2190
+ position = Integer.parseInt(seed.get(0));
2191
+ } catch (NumberFormatException ignored) {
2192
+ // The description's default position stands.
2193
+ }
2194
+ }
2195
+ }
2196
+ Map<String, List<String>> pageParams = cloneQuery(base);
2197
+ pageParams.put(spec.param(), new ArrayList<>(List.of(String.valueOf(position))));
2198
+ Object page = call.call(pageParams).page();
2199
+ Object items = resolvePointer(page, spec.items());
2200
+ List<?> list = items instanceof List<?> found ? found : null;
2201
+ if (list != null) {
2202
+ String serialized = Json.write(list);
2203
+ if (serialized.equals(previousItems)) {
2204
+ throw new IllegalStateException("Pagination did not advance: the operation returned the same page twice");
2205
+ }
2206
+ previousItems = serialized;
2207
+ }
2208
+ if (list == null || list.isEmpty()) {
2209
+ finished = true;
2210
+ return page;
2211
+ }
2212
+ position += spec.style().equals("page") ? 1 : list.size();
2213
+ return page;
2214
+ }
2215
+ };
2216
+ }
2217
+
2218
+ // \u2500\u2500\u2500 Server-Sent Events \u2500\u2500\u2500
2219
+
2220
+ @FunctionalInterface
2221
+ interface SseOpener {
2222
+ HttpResponse<InputStream> open(Map<String, String> extraHeaders);
2223
+ }
2224
+
2225
+ private static ServerSentEvent parseSseFrame(String raw, boolean jsonData) {
2226
+ String event = "message";
2227
+ List<String> dataLines = new ArrayList<>();
2228
+ String id = null;
2229
+ Integer retry = null;
2230
+ boolean sawField = false;
2231
+ for (String line : raw.replace("\\r\\n", "\\n").replace('\\r', '\\n').split("\\n")) {
2232
+ if (line.isEmpty() || line.startsWith(":")) {
2233
+ continue;
2234
+ }
2235
+ int colon = line.indexOf(':');
2236
+ String field = colon < 0 ? line : line.substring(0, colon);
2237
+ String value = colon < 0 ? "" : line.substring(colon + 1);
2238
+ if (value.startsWith(" ")) {
2239
+ value = value.substring(1);
2240
+ }
2241
+ sawField = true;
2242
+ switch (field) {
2243
+ case "event":
2244
+ event = value;
2245
+ break;
2246
+ case "data":
2247
+ dataLines.add(value);
2248
+ break;
2249
+ case "id":
2250
+ id = value;
2251
+ break;
2252
+ case "retry":
2253
+ try {
2254
+ retry = Integer.valueOf(value);
2255
+ } catch (NumberFormatException ignored) {
2256
+ // A non-numeric retry field is ignored, as the spec says.
2257
+ }
2258
+ break;
2259
+ default:
2260
+ }
2261
+ }
2262
+ if (!sawField) {
2263
+ return null;
2264
+ }
2265
+ String text = String.join("\\n", dataLines);
2266
+ Object data = jsonData && !text.isEmpty() ? Json.parse(text) : text;
2267
+ return new ServerSentEvent(event, data, id, retry);
2268
+ }
2269
+
2270
+ /**
2271
+ * Stream server-sent events, reconnecting with \`Last-Event-ID\` on a dropped connection
2272
+ * (a fresh open = fresh auth); a 4xx/5xx or a bad JSON payload ends the stream with an
2273
+ * exception, a clean end of stream ends it quietly.
2274
+ */
2275
+ static Iterable<ServerSentEvent> iterSse(SseOpener open, boolean jsonData) {
2276
+ return () -> new LazyIterator<>() {
2277
+ private InputStream body;
2278
+ private final StringBuilder buffer = new StringBuilder();
2279
+ private final byte[] chunk = new byte[4096];
2280
+ private String lastEventId;
2281
+ private int serverRetry = -1;
2282
+ private int failures;
2283
+ private boolean finished;
2284
+
2285
+ private void connect() {
2286
+ Map<String, String> headers = new LinkedHashMap<>();
2287
+ headers.put("Accept", "text/event-stream");
2288
+ if (lastEventId != null) {
2289
+ headers.put("Last-Event-ID", lastEventId);
2290
+ }
2291
+ HttpResponse<InputStream> response = open.open(headers);
2292
+ if (response.statusCode() >= 400) {
2293
+ throw apiErrorFrom(response, response.uri().toString());
2294
+ }
2295
+ body = response.body();
2296
+ failures = 0;
2297
+ }
2298
+
2299
+ private void backoff() {
2300
+ failures++;
2301
+ long base = serverRetry >= 0 ? serverRetry : 1000;
2302
+ long delay = Math.min(base * (1L << (failures - 1)), 30_000);
2303
+ sleep(Duration.ofMillis(ThreadLocalRandom.current().nextLong(delay + 1)));
2304
+ }
2305
+
2306
+ private ServerSentEvent take(String frame) {
2307
+ ServerSentEvent event = parseSseFrame(frame, jsonData);
2308
+ if (event == null) {
2309
+ return null;
2310
+ }
2311
+ if (event.id() != null) {
2312
+ lastEventId = event.id();
2313
+ }
2314
+ if (event.retry() != null && event.retry() >= 0) {
2315
+ serverRetry = event.retry();
2316
+ }
2317
+ return event;
2318
+ }
2319
+
2320
+ @Override
2321
+ protected ServerSentEvent fetch() {
2322
+ while (!finished) {
2323
+ if (body == null) {
2324
+ try {
2325
+ connect();
2326
+ } catch (UncheckedIOException dropped) {
2327
+ backoff();
2328
+ continue;
2329
+ }
2330
+ }
2331
+ int split = buffer.indexOf("\\n\\n");
2332
+ if (split >= 0) {
2333
+ String frame = buffer.substring(0, split);
2334
+ buffer.delete(0, split + 2);
2335
+ ServerSentEvent event = take(frame);
2336
+ if (event != null) {
2337
+ return event;
2338
+ }
2339
+ continue;
2340
+ }
2341
+ int read;
2342
+ try {
2343
+ read = body.read(chunk);
2344
+ } catch (IOException dropped) {
2345
+ read = -2;
2346
+ }
2347
+ if (read >= 0) {
2348
+ buffer.append(new String(chunk, 0, read, StandardCharsets.UTF_8).replace("\\r\\n", "\\n").replace('\\r', '\\n'));
2349
+ continue;
2350
+ }
2351
+ try {
2352
+ body.close();
2353
+ } catch (IOException ignored) {
2354
+ // The connection is gone either way.
2355
+ }
2356
+ body = null;
2357
+ if (read == -1) {
2358
+ finished = true;
2359
+ String rest = buffer.toString();
2360
+ buffer.setLength(0);
2361
+ return rest.isBlank() ? null : take(rest);
2362
+ }
2363
+ backoff();
2364
+ }
2365
+ return null;
2366
+ }
2367
+ };
2368
+ }
2369
+
2370
+ // \u2500\u2500\u2500 Request bodies \u2500\u2500\u2500
2371
+
2372
+ /** A prepared request body: the bytes or stream to send and the content type to declare, if any. */
2373
+ record Body(Object body, String contentType) {}
2374
+
2375
+ /** Encode a map as \`multipart/form-data\`; nested values become JSON parts. */
2376
+ static Body toMultipart(Map<String, Object> fields) {
2377
+ String boundary = "redocly-" + Long.toHexString(ThreadLocalRandom.current().nextLong()) + Long.toHexString(ThreadLocalRandom.current().nextLong());
2378
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
2379
+ try {
2380
+ for (Map.Entry<String, Object> entry : fields.entrySet()) {
2381
+ Object value = entry.getValue();
2382
+ if (value == null) {
2383
+ continue;
2384
+ }
2385
+ out.write(("--" + boundary + "\\r\\n").getBytes(StandardCharsets.UTF_8));
2386
+ if (value instanceof byte[] bytes) {
2387
+ out.write(("Content-Disposition: form-data; name=\\"" + entry.getKey() + "\\"; filename=\\"" + entry.getKey() + "\\"\\r\\nContent-Type: application/octet-stream\\r\\n\\r\\n").getBytes(StandardCharsets.UTF_8));
2388
+ out.write(bytes);
2389
+ } else if (value instanceof String || value instanceof Number || value instanceof Boolean || value instanceof WireValue) {
2390
+ out.write(("Content-Disposition: form-data; name=\\"" + entry.getKey() + "\\"\\r\\n\\r\\n").getBytes(StandardCharsets.UTF_8));
2391
+ out.write(textValue(value).getBytes(StandardCharsets.UTF_8));
2392
+ } else {
2393
+ out.write(("Content-Disposition: form-data; name=\\"" + entry.getKey() + "\\"\\r\\nContent-Type: application/json\\r\\n\\r\\n").getBytes(StandardCharsets.UTF_8));
2394
+ out.write(Json.write(value).getBytes(StandardCharsets.UTF_8));
2395
+ }
2396
+ out.write("\\r\\n".getBytes(StandardCharsets.UTF_8));
2397
+ }
2398
+ out.write(("--" + boundary + "--\\r\\n").getBytes(StandardCharsets.UTF_8));
2399
+ } catch (IOException exception) {
2400
+ throw new UncheckedIOException(exception);
2401
+ }
2402
+ return new Body(out.toByteArray(), "multipart/form-data; boundary=" + boundary);
2403
+ }
2404
+
2405
+ /**
2406
+ * The body of an untyped multipart operation: an InputStream is streamed and a \`byte[]\`
2407
+ * or String sent as is, all under the caller's Content-Type (the boundary is theirs to
2408
+ * set); a map is encoded with \`toMultipart\`.
2409
+ */
2410
+ static Body multipartBody(Object body) {
2411
+ if (body instanceof InputStream || body instanceof byte[]) {
2412
+ return new Body(body, null);
2413
+ }
2414
+ if (body instanceof String text) {
2415
+ return new Body(text.getBytes(StandardCharsets.UTF_8), null);
2416
+ }
2417
+ return toMultipart(asObject(body instanceof JsonValue typed ? typed.toJson() : body));
2418
+ }
2419
+
2420
+ /** The body of a binary operation: an InputStream is streamed, a \`byte[]\` or String sent as is, anything else JSON-encoded. */
2421
+ static Body binaryBody(Object body, String contentType) {
2422
+ if (body instanceof InputStream || body instanceof byte[]) {
2423
+ return new Body(body, contentType);
2424
+ }
2425
+ if (body instanceof String text) {
2426
+ return new Body(text.getBytes(StandardCharsets.UTF_8), contentType);
2427
+ }
2428
+ return new Body(Json.write(body).getBytes(StandardCharsets.UTF_8), contentType);
2429
+ }
2430
+
2431
+ /** A JSON request body from a typed value. */
2432
+ static Body jsonBody(Object body, String contentType) {
2433
+ Object plain = body instanceof JsonValue typed ? typed.toJson() : body;
2434
+ return new Body(Json.write(plain).getBytes(StandardCharsets.UTF_8), contentType);
2435
+ }
2436
+
2437
+ static Map<String, String> pathParams(String... pairs) {
2438
+ Map<String, String> out = new LinkedHashMap<>();
2439
+ for (int index = 0; index + 1 < pairs.length; index += 2) {
2440
+ out.put(pairs[index], Objects.requireNonNull(pairs[index + 1], "path parameter " + pairs[index]));
2441
+ }
2442
+ return out;
2443
+ }
2444
+ }
2445
+ `;var n=`<?php
921
2446
 
922
2447
  // @redocly/client-generator PHP runtime \u2014 embedded into generated clients.
923
2448
  // PHP >= 8.1, zero Composer dependencies; HTTP over the curl extension.
@@ -1450,7 +2975,7 @@ function binaryBody(mixed $body): mixed
1450
2975
  {
1451
2976
  return is_resource($body) || is_string($body) ? $body : json_encode($body);
1452
2977
  }
1453
- `;var n={"_errors.py":`# Runtime errors and the result-mode envelope for generated Python clients.
2978
+ `;var r={"_errors.py":`# Runtime errors and the result-mode envelope for generated Python clients.
1454
2979
  # Hand-authored once, embedded into every generated client (see
1455
2980
  # scripts/generate-runtime-sources.mjs) \u2014 mirror of the TypeScript runtime's
1456
2981
  # errors.ts, kept semantically in lockstep.
@@ -2394,7 +3919,7 @@ def multipart_arguments(body: Any) -> Dict[str, Any]:
2394
3919
  return {"content": body}
2395
3920
  data, files = to_multipart(body)
2396
3921
  return {"data": data, "files": files}
2397
- `};var r=`import type { Middleware, SettledCall } from '../../../runtime-contract.js';
3922
+ `};var a=`import type { Middleware, SettledCall } from '../../../runtime-contract.js';
2398
3923
 
2399
3924
  const GENERATOR_VERSION: string = 'dev';
2400
3925
  const DEFAULT_ENDPOINT = 'https://otel.cloud.redocly.com/v1/traces';
@@ -2457,7 +3982,9 @@ type ProcessLike = {
2457
3982
  on?: (event: 'beforeExit', listener: () => void) => void;
2458
3983
  };
2459
3984
 
2460
- const activeWindows = new Set<() => Promise<void>>();
3985
+ type OnFailedSend = 'keep' | 'drop';
3986
+
3987
+ const activeWindows = new Set<(onFailedSend: OnFailedSend) => Promise<void>>();
2461
3988
  const pendingSends = new Set<Promise<void>>();
2462
3989
  let exitHookInstalled = false;
2463
3990
 
@@ -2476,8 +4003,12 @@ function telemetryDisabled(): boolean {
2476
4003
  return false;
2477
4004
  }
2478
4005
 
2479
- export async function flush(): Promise<void> {
2480
- for (const flushWindow of activeWindows) void flushWindow();
4006
+ export function flush(): Promise<void> {
4007
+ return flushWindows('keep');
4008
+ }
4009
+
4010
+ async function flushWindows(onFailedSend: OnFailedSend): Promise<void> {
4011
+ for (const flushWindow of activeWindows) void flushWindow(onFailedSend);
2481
4012
  await Promise.all(pendingSends);
2482
4013
  }
2483
4014
 
@@ -2509,32 +4040,51 @@ export function telemetry(options: TelemetryOptions): Middleware {
2509
4040
  let windowStart = 0;
2510
4041
  let timer: ReturnType<typeof setTimeout> | undefined;
2511
4042
 
2512
- async function flushWindow(): Promise<void> {
4043
+ function openWindow(): void {
4044
+ windowStart = Date.now();
4045
+ timer = setTimeout(() => void flushWindow('keep'), intervalMs);
4046
+ unrefTimer(timer);
4047
+ activeWindows.add(flushWindow);
4048
+ }
4049
+
4050
+ async function flushWindow(onFailedSend: OnFailedSend): Promise<void> {
2513
4051
  if (counts.size === 0) return;
2514
4052
  if (timer !== undefined) clearTimeout(timer);
2515
4053
  timer = undefined;
2516
4054
  activeWindows.delete(flushWindow);
2517
4055
  const events = toEvents(counts, windowStart, Date.now() - windowStart, options.sdkName, sdk);
4056
+ const sentCounts = counts;
4057
+ const sentWindowStart = windowStart;
2518
4058
  counts = new Map();
2519
4059
  if (telemetryDisabled()) return;
4060
+ let action: OnFailedSend | undefined;
2520
4061
  const send = Promise.resolve()
2521
4062
  .then(() => sink(events))
2522
- .catch(() => undefined);
4063
+ .catch((error: unknown) => {
4064
+ action = actionForFailedSend(error);
4065
+ });
2523
4066
  pendingSends.add(send);
2524
4067
  await send;
2525
4068
  pendingSends.delete(send);
4069
+ if (action !== 'keep' || onFailedSend === 'drop') return;
4070
+
4071
+ if (counts.size === 0) openWindow();
4072
+ if (sentWindowStart < windowStart) windowStart = sentWindowStart;
4073
+ for (const [operationId, sentOperation] of sentCounts) {
4074
+ const operation = counts.get(operationId);
4075
+ if (operation === undefined) {
4076
+ counts.set(operationId, sentOperation);
4077
+ } else {
4078
+ addCounts(operation, sentOperation);
4079
+ }
4080
+ }
2526
4081
  }
2527
4082
 
2528
4083
  installExitHook();
2529
4084
 
2530
4085
  return {
2531
4086
  onSettled: (ctx, call) => {
2532
- if (counts.size === 0) {
2533
- windowStart = Date.now();
2534
- timer = setTimeout(() => void flushWindow(), intervalMs);
2535
- unrefTimer(timer);
2536
- activeWindows.add(flushWindow);
2537
- }
4087
+ if (counts.size === 0) openWindow();
2538
4088
  let operation = counts.get(ctx.operation.id);
2539
4089
  if (operation === undefined) {
2540
4090
  operation = emptyCounts();
@@ -2548,7 +4098,9 @@ export function telemetry(options: TelemetryOptions): Middleware {
2548
4098
  function installExitHook(): void {
2549
4099
  if (exitHookInstalled) return;
2550
4100
  exitHookInstalled = true;
2551
- nodeProcess()?.on?.('beforeExit', () => void flush());
4101
+ // Node emits \`beforeExit\` again after each send at exit.
4102
+ // Kept counts then cause one more send each time, so a failed send here drops its counts.
4103
+ nodeProcess()?.on?.('beforeExit', () => void flushWindows('drop'));
2552
4104
  }
2553
4105
 
2554
4106
  function unrefTimer(timer: unknown): void {
@@ -2580,6 +4132,18 @@ function record(operation: OperationCounts, call: SettledCall): void {
2580
4132
  }
2581
4133
  }
2582
4134
 
4135
+ function addCounts(target: OperationCounts, source: OperationCounts): void {
4136
+ target.calls += source.calls;
4137
+ target.attempts += source.attempts;
4138
+ target.durationSumMs += source.durationSumMs;
4139
+ for (const statusClass of STATUS_CLASSES) {
4140
+ target.status[statusClass] += source.status[statusClass];
4141
+ }
4142
+ for (const { bucket } of LATENCY_BUCKETS) {
4143
+ target.latency[bucket] += source.latency[bucket];
4144
+ }
4145
+ }
4146
+
2583
4147
  function statusClass(call: SettledCall): StatusClass {
2584
4148
  if (call.response === undefined) {
2585
4149
  if (call.error instanceof Error && call.error.name === 'TimeoutError') return 'timeout';
@@ -2641,6 +4205,29 @@ async function sendOtlp(endpoint: string, events: SdkUsageEvent[]): Promise<void
2641
4205
  signal: AbortSignal.timeout(SEND_TIMEOUT_MS),
2642
4206
  });
2643
4207
  await response.body?.cancel();
4208
+ if (!response.ok) {
4209
+ throw new RejectedSendError(response.status);
4210
+ }
4211
+ }
4212
+
4213
+ class RejectedSendError extends Error {
4214
+ public readonly status: number;
4215
+ constructor(status: number) {
4216
+ super(\`The telemetry endpoint answered \${status}.\`);
4217
+ this.name = 'RejectedSendError';
4218
+ this.status = status;
4219
+ }
4220
+ }
4221
+
4222
+ function actionForFailedSend(error: unknown): OnFailedSend {
4223
+ // The endpoint can have stored the counts of a send that timed out.
4224
+ if (error instanceof Error && error.name === 'TimeoutError') return 'drop';
4225
+ if (error instanceof RejectedSendError) {
4226
+ if (error.status === 408 || error.status === 429 || error.status >= 500) return 'keep';
4227
+ // The endpoint refuses the same counts again.
4228
+ return 'drop';
4229
+ }
4230
+ return 'keep';
2644
4231
  }
2645
4232
 
2646
4233
  type OtlpValue = { stringValue: string } | { intValue: number } | { doubleValue: number };
@@ -2706,4 +4293,4 @@ function randomHex(byteCount: number): string {
2706
4293
  for (const byte of bytes) hex += byte.toString(16).padStart(2, '0');
2707
4294
  return hex;
2708
4295
  }
2709
- `;export{e as a,t as b,n as c,r as d};
4296
+ `;export{e as a,t as b,n as c,r as d,a as e};