@redocly/cli 2.71.2 → 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.
- package/README.md +1 -1
- package/THIRD_PARTY_NOTICES +29 -7
- package/lib/chunks/{AZ5LIJEO.js → 2DXUH2SS.js} +1 -1
- package/lib/chunks/{UYZXI3NO.js → 2X2KHSF3.js} +1 -1
- package/lib/chunks/{BPJTNQBC.js → 42X6IAVM.js} +1 -1
- package/lib/chunks/44US4AGQ.js +13 -0
- package/lib/chunks/{R32HBYOH.js → 4AXMK6B2.js} +1 -1
- package/lib/chunks/4ZCKAKVF.js +199 -0
- package/lib/chunks/{N4GCIKY4.js → 5F7XFPS7.js} +1 -1
- package/lib/chunks/{YF4FMOTE.js → 6234PJEZ.js} +1 -1
- package/lib/chunks/{4H3DPQDW.js → 62SOICN7.js} +1 -1
- package/lib/chunks/{BOTTA3HA.js → 6JFL6L56.js} +1 -1
- package/lib/chunks/{NEWT5A6S.js → 6RTITFQA.js} +1 -1
- package/lib/chunks/{KJNBOXDI.js → 6XDKNFYM.js} +3 -3
- package/lib/chunks/{L325HIGK.js → 76AJ5P4Y.js} +1 -1
- package/lib/chunks/{JRVXIEVC.js → 7HTKK26B.js} +1 -1
- package/lib/chunks/{2TG3DP4I.js → A7XJ7Y3N.js} +1 -1
- package/lib/chunks/AB4FYNJN.js +22 -0
- package/lib/chunks/BJ6Z66KZ.js +7 -0
- package/lib/chunks/{XVS7B4RZ.js → BL2KWYMY.js} +1 -1
- package/lib/chunks/{BIS2ZFGS.js → BTLP7Z6A.js} +1 -1
- package/lib/chunks/{AXGFQS75.js → BXMYSHQY.js} +2 -2
- package/lib/chunks/{JOUEVI3E.js → BY22HEXP.js} +1 -1
- package/lib/chunks/D2BBHPMR.js +7 -0
- package/lib/chunks/{5CR6AOLY.js → D3C6XIMY.js} +3 -3
- package/lib/chunks/{DWXDCKZ7.js → DNDYWJHC.js} +1 -1
- package/lib/chunks/{ORWIUECT.js → E6QTRG6I.js} +1 -1
- package/lib/chunks/EHMG7I34.js +9 -0
- package/lib/chunks/{UTEOWLD5.js → EL6MGCYE.js} +1 -1
- package/lib/chunks/EVCSNWMN.js +7 -0
- package/lib/chunks/{KLGUZBJO.js → FJBULQ4P.js} +1 -1
- package/lib/chunks/{IQLQH23V.js → FLL7MTDS.js} +1 -1
- package/lib/chunks/{CRJEY7P3.js → FM7Y6KMP.js} +1 -1
- package/lib/chunks/{CR5TYFBN.js → FMMOQ6SE.js} +2 -2
- package/lib/chunks/{CZX7WE2R.js → FRC2YJS4.js} +1 -1
- package/lib/chunks/{W7TN2XCW.js → GHJGHHQP.js} +1 -1
- package/lib/chunks/{BH4FA7AH.js → GKPSJEG5.js} +1 -1
- package/lib/chunks/{UVKUPD4U.js → GMMFZFKJ.js} +1 -1
- package/lib/chunks/{MJ2NI6MR.js → GW4PLLFM.js} +1 -1
- package/lib/chunks/{5AVTCY6F.js → HIMEF7J2.js} +1 -1
- package/lib/chunks/{U4MI7HDM.js → HJ5KS42K.js} +1 -1
- package/lib/chunks/{DZT5NLUB.js → HXOTKK2E.js} +1 -1
- package/lib/chunks/{HP7UD4BA.js → IFGXTL5P.js} +1 -1
- package/lib/chunks/{XQEXWC5L.js → IHUHQ7PO.js} +1 -1
- package/lib/chunks/{3FR3456Q.js → IJYEZR53.js} +1 -1
- package/lib/chunks/{JO76DPXR.js → IKSSZYHA.js} +1 -1
- package/lib/chunks/{I4TK6NG7.js → IMNPDC6N.js} +1 -1
- package/lib/chunks/{7JS7JGWP.js → IO7M7MGV.js} +1 -1
- package/lib/chunks/{LY6BDOEF.js → J3TXBQDL.js} +1 -1
- package/lib/chunks/{HRZLSSOF.js → JZV7EL4R.js} +1 -1
- package/lib/chunks/{EHJ3FMW6.js → K6HV4EJS.js} +1 -1
- package/lib/chunks/{OFIQ2EST.js → KVXVR5Y4.js} +33 -33
- package/lib/chunks/{LKNL5LEV.js → L4HVZOFP.js} +1 -1
- package/lib/chunks/{Z6GFQV2Y.js → L5AG5NJZ.js} +1 -1
- package/lib/chunks/{ZXOX7NF2.js → LTJXST6M.js} +1 -1
- package/lib/chunks/MO6NEDYO.js +17 -0
- package/lib/chunks/{FDKY4PGX.js → NDECHUWV.js} +1 -1
- package/lib/chunks/{HMYFTLS7.js → NFGCHP5P.js} +1 -1
- package/lib/chunks/{GFSMOHUE.js → NNBSSORJ.js} +1 -1
- package/lib/chunks/{ADACRYTN.js → OLAN2CVV.js} +1 -1
- package/lib/chunks/{CER2D6ZD.js → OOKU5T7P.js} +7 -7
- package/lib/chunks/{U2F3JOT2.js → OQRUWVII.js} +1 -1
- package/lib/chunks/{WO6IIVA3.js → Q5WCA45H.js} +8 -8
- package/lib/chunks/{6X42UQ3O.js → QGKTPGO7.js} +1 -1
- package/lib/chunks/{DM4EUYBB.js → QQ2PGWE6.js} +2 -2
- package/lib/chunks/QYBWIG5Y.js +59 -0
- package/lib/chunks/{QTSECOQB.js → RC4LK3ZS.js} +2 -2
- package/lib/chunks/{NXREEI2S.js → RL2EIWFM.js} +1 -1
- package/lib/chunks/{4S6LRPCX.js → RQMFL6Q4.js} +1 -1
- package/lib/chunks/S3B562IT.js +7 -0
- package/lib/chunks/{P5T6BIKW.js → SPIFPTTE.js} +1 -1
- package/lib/chunks/SUQ7ZWYJ.js +7 -0
- package/lib/chunks/{VJZDR6KS.js → TFPMT2XD.js} +1529 -4
- package/lib/chunks/{FAFMGLT6.js → TVUYXOSQ.js} +1 -1
- package/lib/chunks/{T77BOGYV.js → UFDBFVRI.js} +1 -1
- package/lib/chunks/{HTFKP5HH.js → UH4Q6U6X.js} +1 -1
- package/lib/chunks/{LET2MPMU.js → UKVHQIBO.js} +1 -1
- package/lib/chunks/{SSEMMWPY.js → VF2YOZ3U.js} +1 -1
- package/lib/chunks/{5HPO77VR.js → VO775J6V.js} +2 -2
- package/lib/chunks/WKBXTA7Y.js +53 -0
- package/lib/chunks/{YWRFPTTI.js → WRGAJXOK.js} +1 -1
- package/lib/chunks/{3EQNI6CT.js → XKENZH7W.js} +1 -1
- package/lib/chunks/{QXXAC25O.js → YB52VXMW.js} +1 -1
- package/lib/chunks/{VPTYPV6P.js → ZSQF6YOF.js} +1 -1
- package/lib/chunks/{TH44RKLO.js → ZYHLWT2C.js} +1 -1
- package/lib/eject-assets/AGENTS.md +1 -1
- package/lib/eject-assets/generators/cli/docs.ts +1 -1
- package/lib/eject-assets/generators/cli/engine-source.ts +1 -1
- package/lib/eject-assets/generators/cli/index.ts +2 -2
- package/lib/eject-assets/generators/cli/render.ts +1 -1
- package/lib/eject-assets/generators/go/client.ts +1 -1
- package/lib/eject-assets/generators/go/descriptor.ts +1 -1
- package/lib/eject-assets/generators/go/index.ts +2 -2
- package/lib/eject-assets/generators/go/models.ts +1 -1
- package/lib/eject-assets/generators/go/naming.ts +1 -1
- package/lib/eject-assets/generators/go/operations.ts +1 -1
- package/lib/eject-assets/generators/go/pagination.ts +1 -1
- package/lib/eject-assets/generators/go/types.ts +1 -1
- package/lib/eject-assets/generators/java/client.ts +66 -0
- package/lib/eject-assets/generators/java/descriptor.ts +72 -0
- package/lib/eject-assets/generators/java/index.ts +245 -0
- package/lib/eject-assets/generators/java/models.ts +362 -0
- package/lib/eject-assets/generators/java/naming.ts +135 -0
- package/lib/eject-assets/generators/java/operations.ts +300 -0
- package/lib/eject-assets/generators/java/pagination.ts +94 -0
- package/lib/eject-assets/generators/java/types.ts +103 -0
- package/lib/eject-assets/generators/mock/faker.ts +1 -1
- package/lib/eject-assets/generators/mock/index.ts +2 -2
- package/lib/eject-assets/generators/mock/render.ts +1 -1
- package/lib/eject-assets/generators/mock/sample.ts +1 -1
- package/lib/eject-assets/generators/mock/values.ts +1 -1
- package/lib/eject-assets/generators/php/client.ts +1 -1
- package/lib/eject-assets/generators/php/descriptor.ts +1 -1
- package/lib/eject-assets/generators/php/index.ts +2 -2
- package/lib/eject-assets/generators/php/models.ts +1 -1
- package/lib/eject-assets/generators/php/naming.ts +1 -1
- package/lib/eject-assets/generators/php/operations.ts +1 -1
- package/lib/eject-assets/generators/php/pagination.ts +1 -1
- package/lib/eject-assets/generators/php/types.ts +1 -1
- package/lib/eject-assets/generators/python/client.ts +1 -1
- package/lib/eject-assets/generators/python/descriptor.ts +1 -1
- package/lib/eject-assets/generators/python/index.ts +2 -2
- package/lib/eject-assets/generators/python/models.ts +1 -1
- package/lib/eject-assets/generators/python/naming.ts +1 -1
- package/lib/eject-assets/generators/python/operations.ts +1 -1
- package/lib/eject-assets/generators/python/pagination.ts +1 -1
- package/lib/eject-assets/generators/python/types.ts +1 -1
- package/lib/eject-assets/generators/swr/index.ts +2 -2
- package/lib/eject-assets/generators/swr/render.ts +1 -1
- package/lib/eject-assets/generators/tanstack-query/index.ts +2 -2
- package/lib/eject-assets/generators/tanstack-query/render.ts +1 -1
- package/lib/eject-assets/generators/transformers/index.ts +2 -2
- package/lib/eject-assets/generators/transformers/render.ts +1 -1
- package/lib/eject-assets/generators/typescript/banner.ts +1 -1
- package/lib/eject-assets/generators/typescript/client.ts +1 -1
- package/lib/eject-assets/generators/typescript/descriptor.ts +1 -1
- package/lib/eject-assets/generators/typescript/index.ts +2 -2
- package/lib/eject-assets/generators/typescript/inline-runtime.ts +1 -1
- package/lib/eject-assets/generators/typescript/operation-signature.ts +1 -1
- package/lib/eject-assets/generators/typescript/operation-types.ts +1 -1
- package/lib/eject-assets/generators/typescript/operations.ts +1 -1
- package/lib/eject-assets/generators/typescript/response-headers.ts +1 -1
- package/lib/eject-assets/generators/typescript/type-guards.ts +1 -1
- package/lib/eject-assets/generators/typescript/types.ts +1 -1
- package/lib/eject-assets/generators/zod/index.ts +2 -2
- package/lib/eject-assets/generators/zod/schemas.ts +1 -1
- package/lib/eject-assets/skills/client-generators/SKILL.md +1 -1
- package/lib/eject-assets/skills/java-generator/SKILL.md +161 -0
- package/lib/index.js +91 -90
- package/lib/skills/redocly-cli/SKILL.md +3 -1
- package/package.json +1 -1
- package/lib/chunks/2ZEKFWJQ.js +0 -13
- package/lib/chunks/3VOQMXIZ.js +0 -7
- package/lib/chunks/7GADNOVY.js +0 -9
- package/lib/chunks/FRQHCFKP.js +0 -199
- package/lib/chunks/J2EPFEBO.js +0 -17
- package/lib/chunks/JZATGVUO.js +0 -53
- package/lib/chunks/KNMNSDHR.js +0 -7
- package/lib/chunks/U5DIFWGH.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
|
|
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
|
|
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
|
|
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';
|
|
@@ -2768,4 +4293,4 @@ function randomHex(byteCount: number): string {
|
|
|
2768
4293
|
for (const byte of bytes) hex += byte.toString(16).padStart(2, '0');
|
|
2769
4294
|
return hex;
|
|
2770
4295
|
}
|
|
2771
|
-
`;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};
|