diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c71c638..e18927e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,14 @@ name: CI on: push: - branches-ignore: - - 'generated' - - 'codegen/**' - - 'integrated/**' - - 'stl-preview-head/**' - - 'stl-preview-base/**' + branches: + - '**' + - '!integrated/**' + - '!stl-preview-head/**' + - '!stl-preview-base/**' + - '!generated' + - '!codegen/**' + - 'codegen/stl/**' pull_request: branches-ignore: - 'stl-preview-head/**' diff --git a/.release-please-manifest.json b/.release-please-manifest.json index f996350..a696b6a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.35" + ".": "0.1.0-alpha.36" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index f98f9e8..18cf9ec 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 20 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/brand-dev%2Fbrand.dev-584d3486a6c5bf7b68dcaacb0bde2ef5f648c158e5c5ebccc7a7684d95abc832.yml -openapi_spec_hash: 29a53e1f96a2c5d9407f1a0938e301bf +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/brand-dev%2Fbrand.dev-67e4ffa39d74649a6ae6b21e9f86cffa83c8a02d640ca6b4d4a3e619b54fbd38.yml +openapi_spec_hash: 762e7ea7ae23297cc6b01f600a485410 config_hash: 4cd3173ea1cce7183640aae49cfbb374 diff --git a/CHANGELOG.md b/CHANGELOG.md index b6492a8..28cdd99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 0.1.0-alpha.36 (2026-03-18) + +Full Changelog: [v0.1.0-alpha.35...v0.1.0-alpha.36](https://github.com/brand-dot-dev/java-sdk/compare/v0.1.0-alpha.35...v0.1.0-alpha.36) + +### Features + +* **api:** api update ([2684d41](https://github.com/brand-dot-dev/java-sdk/commit/2684d410059e02e7dd09c288bd19e707a7221108)) + + +### Bug Fixes + +* **client:** incorrect `Retry-After` parsing ([95a5a2b](https://github.com/brand-dot-dev/java-sdk/commit/95a5a2b76191203166974e66729cd1142cf0fb4a)) + + +### Chores + +* **internal:** tweak CI branches ([7ab78d8](https://github.com/brand-dot-dev/java-sdk/commit/7ab78d8b427c70f67f20bd77b02e36d0ccf6d84b)) + ## 0.1.0-alpha.35 (2026-03-07) Full Changelog: [v0.1.0-alpha.34...v0.1.0-alpha.35](https://github.com/brand-dot-dev/java-sdk/compare/v0.1.0-alpha.34...v0.1.0-alpha.35) diff --git a/README.md b/README.md index 8e37bc1..7d54127 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ -[![Maven Central](https://img.shields.io/maven-central/v/com.branddev.api/brand-dev-java)](https://central.sonatype.com/artifact/com.branddev.api/brand-dev-java/0.1.0-alpha.35) -[![javadoc](https://javadoc.io/badge2/com.branddev.api/brand-dev-java/0.1.0-alpha.35/javadoc.svg)](https://javadoc.io/doc/com.branddev.api/brand-dev-java/0.1.0-alpha.35) +[![Maven Central](https://img.shields.io/maven-central/v/com.branddev.api/brand-dev-java)](https://central.sonatype.com/artifact/com.branddev.api/brand-dev-java/0.1.0-alpha.36) +[![javadoc](https://javadoc.io/badge2/com.branddev.api/brand-dev-java/0.1.0-alpha.36/javadoc.svg)](https://javadoc.io/doc/com.branddev.api/brand-dev-java/0.1.0-alpha.36) @@ -22,7 +22,7 @@ Use the Brand Dev MCP Server to enable AI assistants to interact with this API, -The REST API documentation can be found on [docs.brand.dev](https://docs.brand.dev/). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.branddev.api/brand-dev-java/0.1.0-alpha.35). +The REST API documentation can be found on [docs.brand.dev](https://docs.brand.dev/). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.branddev.api/brand-dev-java/0.1.0-alpha.36). @@ -33,7 +33,7 @@ The REST API documentation can be found on [docs.brand.dev](https://docs.brand.d ### Gradle ```kotlin -implementation("com.branddev.api:brand-dev-java:0.1.0-alpha.35") +implementation("com.branddev.api:brand-dev-java:0.1.0-alpha.36") ``` ### Maven @@ -42,7 +42,7 @@ implementation("com.branddev.api:brand-dev-java:0.1.0-alpha.35") com.branddev.api brand-dev-java - 0.1.0-alpha.35 + 0.1.0-alpha.36 ``` diff --git a/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/http/RetryingHttpClient.kt b/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/http/RetryingHttpClient.kt index ce2a523..3054205 100644 --- a/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/http/RetryingHttpClient.kt +++ b/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/http/RetryingHttpClient.kt @@ -201,7 +201,7 @@ private constructor( ?: headers.values("Retry-After").getOrNull(0)?.let { retryAfter -> retryAfter.toFloatOrNull()?.times(TimeUnit.SECONDS.toNanos(1)) ?: try { - ChronoUnit.MILLIS.between( + ChronoUnit.NANOS.between( OffsetDateTime.now(clock), OffsetDateTime.parse( retryAfter, diff --git a/brand-dev-java-core/src/main/kotlin/com/branddev/api/models/brand/BrandRetrieveByNameParams.kt b/brand-dev-java-core/src/main/kotlin/com/branddev/api/models/brand/BrandRetrieveByNameParams.kt index 825b31d..c320160 100644 --- a/brand-dev-java-core/src/main/kotlin/com/branddev/api/models/brand/BrandRetrieveByNameParams.kt +++ b/brand-dev-java-core/src/main/kotlin/com/branddev/api/models/brand/BrandRetrieveByNameParams.kt @@ -21,6 +21,7 @@ import kotlin.jvm.optionals.getOrNull class BrandRetrieveByNameParams private constructor( private val name: String, + private val countryGl: CountryGl?, private val forceLanguage: ForceLanguage?, private val maxSpeed: Boolean?, private val timeoutMs: Long?, @@ -34,6 +35,12 @@ private constructor( */ fun name(): String = name + /** + * Optional country code (GL parameter) to specify the country. This affects the geographic + * location used for search queries. + */ + fun countryGl(): Optional = Optional.ofNullable(countryGl) + /** Optional parameter to force the language of the retrieved brand data. */ fun forceLanguage(): Optional = Optional.ofNullable(forceLanguage) @@ -75,6 +82,7 @@ private constructor( class Builder internal constructor() { private var name: String? = null + private var countryGl: CountryGl? = null private var forceLanguage: ForceLanguage? = null private var maxSpeed: Boolean? = null private var timeoutMs: Long? = null @@ -84,6 +92,7 @@ private constructor( @JvmSynthetic internal fun from(brandRetrieveByNameParams: BrandRetrieveByNameParams) = apply { name = brandRetrieveByNameParams.name + countryGl = brandRetrieveByNameParams.countryGl forceLanguage = brandRetrieveByNameParams.forceLanguage maxSpeed = brandRetrieveByNameParams.maxSpeed timeoutMs = brandRetrieveByNameParams.timeoutMs @@ -97,6 +106,15 @@ private constructor( */ fun name(name: String) = apply { this.name = name } + /** + * Optional country code (GL parameter) to specify the country. This affects the geographic + * location used for search queries. + */ + fun countryGl(countryGl: CountryGl?) = apply { this.countryGl = countryGl } + + /** Alias for calling [Builder.countryGl] with `countryGl.orElse(null)`. */ + fun countryGl(countryGl: Optional) = countryGl(countryGl.getOrNull()) + /** Optional parameter to force the language of the retrieved brand data. */ fun forceLanguage(forceLanguage: ForceLanguage?) = apply { this.forceLanguage = forceLanguage @@ -253,6 +271,7 @@ private constructor( fun build(): BrandRetrieveByNameParams = BrandRetrieveByNameParams( checkRequired("name", name), + countryGl, forceLanguage, maxSpeed, timeoutMs, @@ -267,6 +286,7 @@ private constructor( QueryParams.builder() .apply { put("name", name) + countryGl?.let { put("country_gl", it.toString()) } forceLanguage?.let { put("force_language", it.toString()) } maxSpeed?.let { put("maxSpeed", it.toString()) } timeoutMs?.let { put("timeoutMS", it.toString()) } @@ -274,6 +294,1561 @@ private constructor( } .build() + /** + * Optional country code (GL parameter) to specify the country. This affects the geographic + * location used for search queries. + */ + class CountryGl @JsonCreator private constructor(private val value: JsonField) : Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that doesn't + * match any known member, and you want to know that value. For example, if the SDK is on an + * older version than the API, then the API may respond with new members that the SDK is + * unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value + + companion object { + + @JvmField val AD = of("ad") + + @JvmField val AE = of("ae") + + @JvmField val AF = of("af") + + @JvmField val AG = of("ag") + + @JvmField val AI = of("ai") + + @JvmField val AL = of("al") + + @JvmField val AM = of("am") + + @JvmField val AN = of("an") + + @JvmField val AO = of("ao") + + @JvmField val AQ = of("aq") + + @JvmField val AR = of("ar") + + @JvmField val AS = of("as") + + @JvmField val AT = of("at") + + @JvmField val AU = of("au") + + @JvmField val AW = of("aw") + + @JvmField val AZ = of("az") + + @JvmField val BA = of("ba") + + @JvmField val BB = of("bb") + + @JvmField val BD = of("bd") + + @JvmField val BE = of("be") + + @JvmField val BF = of("bf") + + @JvmField val BG = of("bg") + + @JvmField val BH = of("bh") + + @JvmField val BI = of("bi") + + @JvmField val BJ = of("bj") + + @JvmField val BM = of("bm") + + @JvmField val BN = of("bn") + + @JvmField val BO = of("bo") + + @JvmField val BR = of("br") + + @JvmField val BS = of("bs") + + @JvmField val BT = of("bt") + + @JvmField val BV = of("bv") + + @JvmField val BW = of("bw") + + @JvmField val BY = of("by") + + @JvmField val BZ = of("bz") + + @JvmField val CA = of("ca") + + @JvmField val CC = of("cc") + + @JvmField val CD = of("cd") + + @JvmField val CF = of("cf") + + @JvmField val CG = of("cg") + + @JvmField val CH = of("ch") + + @JvmField val CI = of("ci") + + @JvmField val CK = of("ck") + + @JvmField val CL = of("cl") + + @JvmField val CM = of("cm") + + @JvmField val CN = of("cn") + + @JvmField val CO = of("co") + + @JvmField val CR = of("cr") + + @JvmField val CU = of("cu") + + @JvmField val CV = of("cv") + + @JvmField val CX = of("cx") + + @JvmField val CY = of("cy") + + @JvmField val CZ = of("cz") + + @JvmField val DE = of("de") + + @JvmField val DJ = of("dj") + + @JvmField val DK = of("dk") + + @JvmField val DM = of("dm") + + @JvmField val DO = of("do") + + @JvmField val DZ = of("dz") + + @JvmField val EC = of("ec") + + @JvmField val EE = of("ee") + + @JvmField val EG = of("eg") + + @JvmField val EH = of("eh") + + @JvmField val ER = of("er") + + @JvmField val ES = of("es") + + @JvmField val ET = of("et") + + @JvmField val FI = of("fi") + + @JvmField val FJ = of("fj") + + @JvmField val FK = of("fk") + + @JvmField val FM = of("fm") + + @JvmField val FO = of("fo") + + @JvmField val FR = of("fr") + + @JvmField val GA = of("ga") + + @JvmField val GB = of("gb") + + @JvmField val GD = of("gd") + + @JvmField val GE = of("ge") + + @JvmField val GF = of("gf") + + @JvmField val GH = of("gh") + + @JvmField val GI = of("gi") + + @JvmField val GL = of("gl") + + @JvmField val GM = of("gm") + + @JvmField val GN = of("gn") + + @JvmField val GP = of("gp") + + @JvmField val GQ = of("gq") + + @JvmField val GR = of("gr") + + @JvmField val GS = of("gs") + + @JvmField val GT = of("gt") + + @JvmField val GU = of("gu") + + @JvmField val GW = of("gw") + + @JvmField val GY = of("gy") + + @JvmField val HK = of("hk") + + @JvmField val HM = of("hm") + + @JvmField val HN = of("hn") + + @JvmField val HR = of("hr") + + @JvmField val HT = of("ht") + + @JvmField val HU = of("hu") + + @JvmField val ID = of("id") + + @JvmField val IE = of("ie") + + @JvmField val IL = of("il") + + @JvmField val IN = of("in") + + @JvmField val IO = of("io") + + @JvmField val IQ = of("iq") + + @JvmField val IR = of("ir") + + @JvmField val IS = of("is") + + @JvmField val IT = of("it") + + @JvmField val JM = of("jm") + + @JvmField val JO = of("jo") + + @JvmField val JP = of("jp") + + @JvmField val KE = of("ke") + + @JvmField val KG = of("kg") + + @JvmField val KH = of("kh") + + @JvmField val KI = of("ki") + + @JvmField val KM = of("km") + + @JvmField val KN = of("kn") + + @JvmField val KP = of("kp") + + @JvmField val KR = of("kr") + + @JvmField val KW = of("kw") + + @JvmField val KY = of("ky") + + @JvmField val KZ = of("kz") + + @JvmField val LA = of("la") + + @JvmField val LB = of("lb") + + @JvmField val LC = of("lc") + + @JvmField val LI = of("li") + + @JvmField val LK = of("lk") + + @JvmField val LR = of("lr") + + @JvmField val LS = of("ls") + + @JvmField val LT = of("lt") + + @JvmField val LU = of("lu") + + @JvmField val LV = of("lv") + + @JvmField val LY = of("ly") + + @JvmField val MA = of("ma") + + @JvmField val MC = of("mc") + + @JvmField val MD = of("md") + + @JvmField val MG = of("mg") + + @JvmField val MH = of("mh") + + @JvmField val MK = of("mk") + + @JvmField val ML = of("ml") + + @JvmField val MM = of("mm") + + @JvmField val MN = of("mn") + + @JvmField val MO = of("mo") + + @JvmField val MP = of("mp") + + @JvmField val MQ = of("mq") + + @JvmField val MR = of("mr") + + @JvmField val MS = of("ms") + + @JvmField val MT = of("mt") + + @JvmField val MU = of("mu") + + @JvmField val MV = of("mv") + + @JvmField val MW = of("mw") + + @JvmField val MX = of("mx") + + @JvmField val MY = of("my") + + @JvmField val MZ = of("mz") + + @JvmField val NA = of("na") + + @JvmField val NC = of("nc") + + @JvmField val NE = of("ne") + + @JvmField val NF = of("nf") + + @JvmField val NG = of("ng") + + @JvmField val NI = of("ni") + + @JvmField val NL = of("nl") + + @JvmField val NO = of("no") + + @JvmField val NP = of("np") + + @JvmField val NR = of("nr") + + @JvmField val NU = of("nu") + + @JvmField val NZ = of("nz") + + @JvmField val OM = of("om") + + @JvmField val PA = of("pa") + + @JvmField val PE = of("pe") + + @JvmField val PF = of("pf") + + @JvmField val PG = of("pg") + + @JvmField val PH = of("ph") + + @JvmField val PK = of("pk") + + @JvmField val PL = of("pl") + + @JvmField val PM = of("pm") + + @JvmField val PN = of("pn") + + @JvmField val PR = of("pr") + + @JvmField val PS = of("ps") + + @JvmField val PT = of("pt") + + @JvmField val PW = of("pw") + + @JvmField val PY = of("py") + + @JvmField val QA = of("qa") + + @JvmField val RE = of("re") + + @JvmField val RO = of("ro") + + @JvmField val RS = of("rs") + + @JvmField val RU = of("ru") + + @JvmField val RW = of("rw") + + @JvmField val SA = of("sa") + + @JvmField val SB = of("sb") + + @JvmField val SC = of("sc") + + @JvmField val SD = of("sd") + + @JvmField val SE = of("se") + + @JvmField val SG = of("sg") + + @JvmField val SH = of("sh") + + @JvmField val SI = of("si") + + @JvmField val SJ = of("sj") + + @JvmField val SK = of("sk") + + @JvmField val SL = of("sl") + + @JvmField val SM = of("sm") + + @JvmField val SN = of("sn") + + @JvmField val SO = of("so") + + @JvmField val SR = of("sr") + + @JvmField val ST = of("st") + + @JvmField val SV = of("sv") + + @JvmField val SY = of("sy") + + @JvmField val SZ = of("sz") + + @JvmField val TC = of("tc") + + @JvmField val TD = of("td") + + @JvmField val TF = of("tf") + + @JvmField val TG = of("tg") + + @JvmField val TH = of("th") + + @JvmField val TJ = of("tj") + + @JvmField val TK = of("tk") + + @JvmField val TL = of("tl") + + @JvmField val TM = of("tm") + + @JvmField val TN = of("tn") + + @JvmField val TO = of("to") + + @JvmField val TR = of("tr") + + @JvmField val TT = of("tt") + + @JvmField val TV = of("tv") + + @JvmField val TW = of("tw") + + @JvmField val TZ = of("tz") + + @JvmField val UA = of("ua") + + @JvmField val UG = of("ug") + + @JvmField val UM = of("um") + + @JvmField val US = of("us") + + @JvmField val UY = of("uy") + + @JvmField val UZ = of("uz") + + @JvmField val VA = of("va") + + @JvmField val VC = of("vc") + + @JvmField val VE = of("ve") + + @JvmField val VG = of("vg") + + @JvmField val VI = of("vi") + + @JvmField val VN = of("vn") + + @JvmField val VU = of("vu") + + @JvmField val WF = of("wf") + + @JvmField val WS = of("ws") + + @JvmField val YE = of("ye") + + @JvmField val YT = of("yt") + + @JvmField val ZA = of("za") + + @JvmField val ZM = of("zm") + + @JvmField val ZW = of("zw") + + @JvmStatic fun of(value: String) = CountryGl(JsonField.of(value)) + } + + /** An enum containing [CountryGl]'s known values. */ + enum class Known { + AD, + AE, + AF, + AG, + AI, + AL, + AM, + AN, + AO, + AQ, + AR, + AS, + AT, + AU, + AW, + AZ, + BA, + BB, + BD, + BE, + BF, + BG, + BH, + BI, + BJ, + BM, + BN, + BO, + BR, + BS, + BT, + BV, + BW, + BY, + BZ, + CA, + CC, + CD, + CF, + CG, + CH, + CI, + CK, + CL, + CM, + CN, + CO, + CR, + CU, + CV, + CX, + CY, + CZ, + DE, + DJ, + DK, + DM, + DO, + DZ, + EC, + EE, + EG, + EH, + ER, + ES, + ET, + FI, + FJ, + FK, + FM, + FO, + FR, + GA, + GB, + GD, + GE, + GF, + GH, + GI, + GL, + GM, + GN, + GP, + GQ, + GR, + GS, + GT, + GU, + GW, + GY, + HK, + HM, + HN, + HR, + HT, + HU, + ID, + IE, + IL, + IN, + IO, + IQ, + IR, + IS, + IT, + JM, + JO, + JP, + KE, + KG, + KH, + KI, + KM, + KN, + KP, + KR, + KW, + KY, + KZ, + LA, + LB, + LC, + LI, + LK, + LR, + LS, + LT, + LU, + LV, + LY, + MA, + MC, + MD, + MG, + MH, + MK, + ML, + MM, + MN, + MO, + MP, + MQ, + MR, + MS, + MT, + MU, + MV, + MW, + MX, + MY, + MZ, + NA, + NC, + NE, + NF, + NG, + NI, + NL, + NO, + NP, + NR, + NU, + NZ, + OM, + PA, + PE, + PF, + PG, + PH, + PK, + PL, + PM, + PN, + PR, + PS, + PT, + PW, + PY, + QA, + RE, + RO, + RS, + RU, + RW, + SA, + SB, + SC, + SD, + SE, + SG, + SH, + SI, + SJ, + SK, + SL, + SM, + SN, + SO, + SR, + ST, + SV, + SY, + SZ, + TC, + TD, + TF, + TG, + TH, + TJ, + TK, + TL, + TM, + TN, + TO, + TR, + TT, + TV, + TW, + TZ, + UA, + UG, + UM, + US, + UY, + UZ, + VA, + VC, + VE, + VG, + VI, + VN, + VU, + WF, + WS, + YE, + YT, + ZA, + ZM, + ZW, + } + + /** + * An enum containing [CountryGl]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [CountryGl] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For example, if the + * SDK is on an older version than the API, then the API may respond with new members that + * the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + AD, + AE, + AF, + AG, + AI, + AL, + AM, + AN, + AO, + AQ, + AR, + AS, + AT, + AU, + AW, + AZ, + BA, + BB, + BD, + BE, + BF, + BG, + BH, + BI, + BJ, + BM, + BN, + BO, + BR, + BS, + BT, + BV, + BW, + BY, + BZ, + CA, + CC, + CD, + CF, + CG, + CH, + CI, + CK, + CL, + CM, + CN, + CO, + CR, + CU, + CV, + CX, + CY, + CZ, + DE, + DJ, + DK, + DM, + DO, + DZ, + EC, + EE, + EG, + EH, + ER, + ES, + ET, + FI, + FJ, + FK, + FM, + FO, + FR, + GA, + GB, + GD, + GE, + GF, + GH, + GI, + GL, + GM, + GN, + GP, + GQ, + GR, + GS, + GT, + GU, + GW, + GY, + HK, + HM, + HN, + HR, + HT, + HU, + ID, + IE, + IL, + IN, + IO, + IQ, + IR, + IS, + IT, + JM, + JO, + JP, + KE, + KG, + KH, + KI, + KM, + KN, + KP, + KR, + KW, + KY, + KZ, + LA, + LB, + LC, + LI, + LK, + LR, + LS, + LT, + LU, + LV, + LY, + MA, + MC, + MD, + MG, + MH, + MK, + ML, + MM, + MN, + MO, + MP, + MQ, + MR, + MS, + MT, + MU, + MV, + MW, + MX, + MY, + MZ, + NA, + NC, + NE, + NF, + NG, + NI, + NL, + NO, + NP, + NR, + NU, + NZ, + OM, + PA, + PE, + PF, + PG, + PH, + PK, + PL, + PM, + PN, + PR, + PS, + PT, + PW, + PY, + QA, + RE, + RO, + RS, + RU, + RW, + SA, + SB, + SC, + SD, + SE, + SG, + SH, + SI, + SJ, + SK, + SL, + SM, + SN, + SO, + SR, + ST, + SV, + SY, + SZ, + TC, + TD, + TF, + TG, + TH, + TJ, + TK, + TL, + TM, + TN, + TO, + TR, + TT, + TV, + TW, + TZ, + UA, + UG, + UM, + US, + UY, + UZ, + VA, + VC, + VE, + VG, + VI, + VN, + VU, + WF, + WS, + YE, + YT, + ZA, + ZM, + ZW, + /** + * An enum member indicating that [CountryGl] was instantiated with an unknown value. + */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or [Value._UNKNOWN] + * if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or if you want + * to throw for the unknown case. + */ + fun value(): Value = + when (this) { + AD -> Value.AD + AE -> Value.AE + AF -> Value.AF + AG -> Value.AG + AI -> Value.AI + AL -> Value.AL + AM -> Value.AM + AN -> Value.AN + AO -> Value.AO + AQ -> Value.AQ + AR -> Value.AR + AS -> Value.AS + AT -> Value.AT + AU -> Value.AU + AW -> Value.AW + AZ -> Value.AZ + BA -> Value.BA + BB -> Value.BB + BD -> Value.BD + BE -> Value.BE + BF -> Value.BF + BG -> Value.BG + BH -> Value.BH + BI -> Value.BI + BJ -> Value.BJ + BM -> Value.BM + BN -> Value.BN + BO -> Value.BO + BR -> Value.BR + BS -> Value.BS + BT -> Value.BT + BV -> Value.BV + BW -> Value.BW + BY -> Value.BY + BZ -> Value.BZ + CA -> Value.CA + CC -> Value.CC + CD -> Value.CD + CF -> Value.CF + CG -> Value.CG + CH -> Value.CH + CI -> Value.CI + CK -> Value.CK + CL -> Value.CL + CM -> Value.CM + CN -> Value.CN + CO -> Value.CO + CR -> Value.CR + CU -> Value.CU + CV -> Value.CV + CX -> Value.CX + CY -> Value.CY + CZ -> Value.CZ + DE -> Value.DE + DJ -> Value.DJ + DK -> Value.DK + DM -> Value.DM + DO -> Value.DO + DZ -> Value.DZ + EC -> Value.EC + EE -> Value.EE + EG -> Value.EG + EH -> Value.EH + ER -> Value.ER + ES -> Value.ES + ET -> Value.ET + FI -> Value.FI + FJ -> Value.FJ + FK -> Value.FK + FM -> Value.FM + FO -> Value.FO + FR -> Value.FR + GA -> Value.GA + GB -> Value.GB + GD -> Value.GD + GE -> Value.GE + GF -> Value.GF + GH -> Value.GH + GI -> Value.GI + GL -> Value.GL + GM -> Value.GM + GN -> Value.GN + GP -> Value.GP + GQ -> Value.GQ + GR -> Value.GR + GS -> Value.GS + GT -> Value.GT + GU -> Value.GU + GW -> Value.GW + GY -> Value.GY + HK -> Value.HK + HM -> Value.HM + HN -> Value.HN + HR -> Value.HR + HT -> Value.HT + HU -> Value.HU + ID -> Value.ID + IE -> Value.IE + IL -> Value.IL + IN -> Value.IN + IO -> Value.IO + IQ -> Value.IQ + IR -> Value.IR + IS -> Value.IS + IT -> Value.IT + JM -> Value.JM + JO -> Value.JO + JP -> Value.JP + KE -> Value.KE + KG -> Value.KG + KH -> Value.KH + KI -> Value.KI + KM -> Value.KM + KN -> Value.KN + KP -> Value.KP + KR -> Value.KR + KW -> Value.KW + KY -> Value.KY + KZ -> Value.KZ + LA -> Value.LA + LB -> Value.LB + LC -> Value.LC + LI -> Value.LI + LK -> Value.LK + LR -> Value.LR + LS -> Value.LS + LT -> Value.LT + LU -> Value.LU + LV -> Value.LV + LY -> Value.LY + MA -> Value.MA + MC -> Value.MC + MD -> Value.MD + MG -> Value.MG + MH -> Value.MH + MK -> Value.MK + ML -> Value.ML + MM -> Value.MM + MN -> Value.MN + MO -> Value.MO + MP -> Value.MP + MQ -> Value.MQ + MR -> Value.MR + MS -> Value.MS + MT -> Value.MT + MU -> Value.MU + MV -> Value.MV + MW -> Value.MW + MX -> Value.MX + MY -> Value.MY + MZ -> Value.MZ + NA -> Value.NA + NC -> Value.NC + NE -> Value.NE + NF -> Value.NF + NG -> Value.NG + NI -> Value.NI + NL -> Value.NL + NO -> Value.NO + NP -> Value.NP + NR -> Value.NR + NU -> Value.NU + NZ -> Value.NZ + OM -> Value.OM + PA -> Value.PA + PE -> Value.PE + PF -> Value.PF + PG -> Value.PG + PH -> Value.PH + PK -> Value.PK + PL -> Value.PL + PM -> Value.PM + PN -> Value.PN + PR -> Value.PR + PS -> Value.PS + PT -> Value.PT + PW -> Value.PW + PY -> Value.PY + QA -> Value.QA + RE -> Value.RE + RO -> Value.RO + RS -> Value.RS + RU -> Value.RU + RW -> Value.RW + SA -> Value.SA + SB -> Value.SB + SC -> Value.SC + SD -> Value.SD + SE -> Value.SE + SG -> Value.SG + SH -> Value.SH + SI -> Value.SI + SJ -> Value.SJ + SK -> Value.SK + SL -> Value.SL + SM -> Value.SM + SN -> Value.SN + SO -> Value.SO + SR -> Value.SR + ST -> Value.ST + SV -> Value.SV + SY -> Value.SY + SZ -> Value.SZ + TC -> Value.TC + TD -> Value.TD + TF -> Value.TF + TG -> Value.TG + TH -> Value.TH + TJ -> Value.TJ + TK -> Value.TK + TL -> Value.TL + TM -> Value.TM + TN -> Value.TN + TO -> Value.TO + TR -> Value.TR + TT -> Value.TT + TV -> Value.TV + TW -> Value.TW + TZ -> Value.TZ + UA -> Value.UA + UG -> Value.UG + UM -> Value.UM + US -> Value.US + UY -> Value.UY + UZ -> Value.UZ + VA -> Value.VA + VC -> Value.VC + VE -> Value.VE + VG -> Value.VG + VI -> Value.VI + VN -> Value.VN + VU -> Value.VU + WF -> Value.WF + WS -> Value.WS + YE -> Value.YE + YT -> Value.YT + ZA -> Value.ZA + ZM -> Value.ZM + ZW -> Value.ZW + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known and don't + * want to throw for the unknown case. + * + * @throws BrandDevInvalidDataException if this class instance's value is a not a known + * member. + */ + fun known(): Known = + when (this) { + AD -> Known.AD + AE -> Known.AE + AF -> Known.AF + AG -> Known.AG + AI -> Known.AI + AL -> Known.AL + AM -> Known.AM + AN -> Known.AN + AO -> Known.AO + AQ -> Known.AQ + AR -> Known.AR + AS -> Known.AS + AT -> Known.AT + AU -> Known.AU + AW -> Known.AW + AZ -> Known.AZ + BA -> Known.BA + BB -> Known.BB + BD -> Known.BD + BE -> Known.BE + BF -> Known.BF + BG -> Known.BG + BH -> Known.BH + BI -> Known.BI + BJ -> Known.BJ + BM -> Known.BM + BN -> Known.BN + BO -> Known.BO + BR -> Known.BR + BS -> Known.BS + BT -> Known.BT + BV -> Known.BV + BW -> Known.BW + BY -> Known.BY + BZ -> Known.BZ + CA -> Known.CA + CC -> Known.CC + CD -> Known.CD + CF -> Known.CF + CG -> Known.CG + CH -> Known.CH + CI -> Known.CI + CK -> Known.CK + CL -> Known.CL + CM -> Known.CM + CN -> Known.CN + CO -> Known.CO + CR -> Known.CR + CU -> Known.CU + CV -> Known.CV + CX -> Known.CX + CY -> Known.CY + CZ -> Known.CZ + DE -> Known.DE + DJ -> Known.DJ + DK -> Known.DK + DM -> Known.DM + DO -> Known.DO + DZ -> Known.DZ + EC -> Known.EC + EE -> Known.EE + EG -> Known.EG + EH -> Known.EH + ER -> Known.ER + ES -> Known.ES + ET -> Known.ET + FI -> Known.FI + FJ -> Known.FJ + FK -> Known.FK + FM -> Known.FM + FO -> Known.FO + FR -> Known.FR + GA -> Known.GA + GB -> Known.GB + GD -> Known.GD + GE -> Known.GE + GF -> Known.GF + GH -> Known.GH + GI -> Known.GI + GL -> Known.GL + GM -> Known.GM + GN -> Known.GN + GP -> Known.GP + GQ -> Known.GQ + GR -> Known.GR + GS -> Known.GS + GT -> Known.GT + GU -> Known.GU + GW -> Known.GW + GY -> Known.GY + HK -> Known.HK + HM -> Known.HM + HN -> Known.HN + HR -> Known.HR + HT -> Known.HT + HU -> Known.HU + ID -> Known.ID + IE -> Known.IE + IL -> Known.IL + IN -> Known.IN + IO -> Known.IO + IQ -> Known.IQ + IR -> Known.IR + IS -> Known.IS + IT -> Known.IT + JM -> Known.JM + JO -> Known.JO + JP -> Known.JP + KE -> Known.KE + KG -> Known.KG + KH -> Known.KH + KI -> Known.KI + KM -> Known.KM + KN -> Known.KN + KP -> Known.KP + KR -> Known.KR + KW -> Known.KW + KY -> Known.KY + KZ -> Known.KZ + LA -> Known.LA + LB -> Known.LB + LC -> Known.LC + LI -> Known.LI + LK -> Known.LK + LR -> Known.LR + LS -> Known.LS + LT -> Known.LT + LU -> Known.LU + LV -> Known.LV + LY -> Known.LY + MA -> Known.MA + MC -> Known.MC + MD -> Known.MD + MG -> Known.MG + MH -> Known.MH + MK -> Known.MK + ML -> Known.ML + MM -> Known.MM + MN -> Known.MN + MO -> Known.MO + MP -> Known.MP + MQ -> Known.MQ + MR -> Known.MR + MS -> Known.MS + MT -> Known.MT + MU -> Known.MU + MV -> Known.MV + MW -> Known.MW + MX -> Known.MX + MY -> Known.MY + MZ -> Known.MZ + NA -> Known.NA + NC -> Known.NC + NE -> Known.NE + NF -> Known.NF + NG -> Known.NG + NI -> Known.NI + NL -> Known.NL + NO -> Known.NO + NP -> Known.NP + NR -> Known.NR + NU -> Known.NU + NZ -> Known.NZ + OM -> Known.OM + PA -> Known.PA + PE -> Known.PE + PF -> Known.PF + PG -> Known.PG + PH -> Known.PH + PK -> Known.PK + PL -> Known.PL + PM -> Known.PM + PN -> Known.PN + PR -> Known.PR + PS -> Known.PS + PT -> Known.PT + PW -> Known.PW + PY -> Known.PY + QA -> Known.QA + RE -> Known.RE + RO -> Known.RO + RS -> Known.RS + RU -> Known.RU + RW -> Known.RW + SA -> Known.SA + SB -> Known.SB + SC -> Known.SC + SD -> Known.SD + SE -> Known.SE + SG -> Known.SG + SH -> Known.SH + SI -> Known.SI + SJ -> Known.SJ + SK -> Known.SK + SL -> Known.SL + SM -> Known.SM + SN -> Known.SN + SO -> Known.SO + SR -> Known.SR + ST -> Known.ST + SV -> Known.SV + SY -> Known.SY + SZ -> Known.SZ + TC -> Known.TC + TD -> Known.TD + TF -> Known.TF + TG -> Known.TG + TH -> Known.TH + TJ -> Known.TJ + TK -> Known.TK + TL -> Known.TL + TM -> Known.TM + TN -> Known.TN + TO -> Known.TO + TR -> Known.TR + TT -> Known.TT + TV -> Known.TV + TW -> Known.TW + TZ -> Known.TZ + UA -> Known.UA + UG -> Known.UG + UM -> Known.UM + US -> Known.US + UY -> Known.UY + UZ -> Known.UZ + VA -> Known.VA + VC -> Known.VC + VE -> Known.VE + VG -> Known.VG + VI -> Known.VI + VN -> Known.VN + VU -> Known.VU + WF -> Known.WF + WS -> Known.WS + YE -> Known.YE + YT -> Known.YT + ZA -> Known.ZA + ZM -> Known.ZM + ZW -> Known.ZW + else -> throw BrandDevInvalidDataException("Unknown CountryGl: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for debugging + * and generally doesn't throw. + * + * @throws BrandDevInvalidDataException if this class instance's value does not have the + * expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + BrandDevInvalidDataException("Value is not a String") + } + + private var validated: Boolean = false + + fun validate(): CountryGl = apply { + if (validated) { + return@apply + } + + known() + validated = true + } + + fun isValid(): Boolean = + try { + validate() + true + } catch (e: BrandDevInvalidDataException) { + false + } + + /** + * Returns a score indicating how many valid values are contained in this object + * recursively. + * + * Used for best match union deserialization. + */ + @JvmSynthetic internal fun validity(): Int = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is CountryGl && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + /** Optional parameter to force the language of the retrieved brand data. */ class ForceLanguage @JsonCreator private constructor(private val value: JsonField) : Enum { @@ -713,6 +2288,7 @@ private constructor( return other is BrandRetrieveByNameParams && name == other.name && + countryGl == other.countryGl && forceLanguage == other.forceLanguage && maxSpeed == other.maxSpeed && timeoutMs == other.timeoutMs && @@ -723,6 +2299,7 @@ private constructor( override fun hashCode(): Int = Objects.hash( name, + countryGl, forceLanguage, maxSpeed, timeoutMs, @@ -731,5 +2308,5 @@ private constructor( ) override fun toString() = - "BrandRetrieveByNameParams{name=$name, forceLanguage=$forceLanguage, maxSpeed=$maxSpeed, timeoutMs=$timeoutMs, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}" + "BrandRetrieveByNameParams{name=$name, countryGl=$countryGl, forceLanguage=$forceLanguage, maxSpeed=$maxSpeed, timeoutMs=$timeoutMs, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}" } diff --git a/brand-dev-java-core/src/test/kotlin/com/branddev/api/core/http/RetryingHttpClientTest.kt b/brand-dev-java-core/src/test/kotlin/com/branddev/api/core/http/RetryingHttpClientTest.kt index fabfd84..cf8eff9 100644 --- a/brand-dev-java-core/src/test/kotlin/com/branddev/api/core/http/RetryingHttpClientTest.kt +++ b/brand-dev-java-core/src/test/kotlin/com/branddev/api/core/http/RetryingHttpClientTest.kt @@ -20,7 +20,11 @@ import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo import com.github.tomakehurst.wiremock.junit5.WireMockTest import com.github.tomakehurst.wiremock.stubbing.Scenario import java.io.InputStream +import java.time.Clock import java.time.Duration +import java.time.OffsetDateTime +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter import java.util.concurrent.CompletableFuture import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach @@ -36,6 +40,21 @@ internal class RetryingHttpClientTest { private lateinit var baseUrl: String private lateinit var httpClient: HttpClient + private class RecordingSleeper : Sleeper { + val durations = mutableListOf() + + override fun sleep(duration: Duration) { + durations.add(duration) + } + + override fun sleepAsync(duration: Duration): CompletableFuture { + durations.add(duration) + return CompletableFuture.completedFuture(null) + } + + override fun close() {} + } + @BeforeEach fun beforeEach(wmRuntimeInfo: WireMockRuntimeInfo) { baseUrl = wmRuntimeInfo.httpBaseUrl @@ -86,7 +105,8 @@ internal class RetryingHttpClientTest { @ValueSource(booleans = [false, true]) fun execute(async: Boolean) { stubFor(post(urlPathEqualTo("/something")).willReturn(ok())) - val retryingClient = retryingHttpClientBuilder().build() + val sleeper = RecordingSleeper() + val retryingClient = retryingHttpClientBuilder(sleeper).build() val response = retryingClient.execute( @@ -100,6 +120,7 @@ internal class RetryingHttpClientTest { assertThat(response.statusCode()).isEqualTo(200) verify(1, postRequestedFor(urlPathEqualTo("/something"))) + assertThat(sleeper.durations).isEmpty() assertNoResponseLeaks() } @@ -111,8 +132,12 @@ internal class RetryingHttpClientTest { .withHeader("X-Some-Header", matching("stainless-java-retry-.+")) .willReturn(ok()) ) + val sleeper = RecordingSleeper() val retryingClient = - retryingHttpClientBuilder().maxRetries(2).idempotencyHeader("X-Some-Header").build() + retryingHttpClientBuilder(sleeper) + .maxRetries(2) + .idempotencyHeader("X-Some-Header") + .build() val response = retryingClient.execute( @@ -126,20 +151,20 @@ internal class RetryingHttpClientTest { assertThat(response.statusCode()).isEqualTo(200) verify(1, postRequestedFor(urlPathEqualTo("/something"))) + assertThat(sleeper.durations).isEmpty() assertNoResponseLeaks() } @ParameterizedTest @ValueSource(booleans = [false, true]) fun execute_withRetryAfterHeader(async: Boolean) { + val retryAfterDate = "Wed, 21 Oct 2015 07:28:00 GMT" stubFor( post(urlPathEqualTo("/something")) // First we fail with a retry after header given as a date .inScenario("foo") .whenScenarioStateIs(Scenario.STARTED) - .willReturn( - serviceUnavailable().withHeader("Retry-After", "Wed, 21 Oct 2015 07:28:00 GMT") - ) + .willReturn(serviceUnavailable().withHeader("Retry-After", retryAfterDate)) .willSetStateTo("RETRY_AFTER_DATE") ) stubFor( @@ -158,7 +183,13 @@ internal class RetryingHttpClientTest { .willReturn(ok()) .willSetStateTo("COMPLETED") ) - val retryingClient = retryingHttpClientBuilder().maxRetries(2).build() + // Fix the clock to 5 seconds before the Retry-After date so the date-based backoff is + // deterministic. + val retryAfterDateTime = + OffsetDateTime.parse(retryAfterDate, DateTimeFormatter.RFC_1123_DATE_TIME) + val clock = Clock.fixed(retryAfterDateTime.minusSeconds(5).toInstant(), ZoneOffset.UTC) + val sleeper = RecordingSleeper() + val retryingClient = retryingHttpClientBuilder(sleeper, clock).maxRetries(2).build() val response = retryingClient.execute( @@ -186,19 +217,20 @@ internal class RetryingHttpClientTest { postRequestedFor(urlPathEqualTo("/something")) .withHeader("x-stainless-retry-count", equalTo("2")), ) + assertThat(sleeper.durations) + .containsExactly(Duration.ofSeconds(5), Duration.ofMillis(1234)) assertNoResponseLeaks() } @ParameterizedTest @ValueSource(booleans = [false, true]) fun execute_withOverwrittenRetryCountHeader(async: Boolean) { + val retryAfterDate = "Wed, 21 Oct 2015 07:28:00 GMT" stubFor( post(urlPathEqualTo("/something")) .inScenario("foo") // first we fail with a retry after header given as a date .whenScenarioStateIs(Scenario.STARTED) - .willReturn( - serviceUnavailable().withHeader("Retry-After", "Wed, 21 Oct 2015 07:28:00 GMT") - ) + .willReturn(serviceUnavailable().withHeader("Retry-After", retryAfterDate)) .willSetStateTo("RETRY_AFTER_DATE") ) stubFor( @@ -208,7 +240,11 @@ internal class RetryingHttpClientTest { .willReturn(ok()) .willSetStateTo("COMPLETED") ) - val retryingClient = retryingHttpClientBuilder().maxRetries(2).build() + val retryAfterDateTime = + OffsetDateTime.parse(retryAfterDate, DateTimeFormatter.RFC_1123_DATE_TIME) + val clock = Clock.fixed(retryAfterDateTime.minusSeconds(5).toInstant(), ZoneOffset.UTC) + val sleeper = RecordingSleeper() + val retryingClient = retryingHttpClientBuilder(sleeper, clock).maxRetries(2).build() val response = retryingClient.execute( @@ -227,6 +263,7 @@ internal class RetryingHttpClientTest { postRequestedFor(urlPathEqualTo("/something")) .withHeader("x-stainless-retry-count", equalTo("42")), ) + assertThat(sleeper.durations).containsExactly(Duration.ofSeconds(5)) assertNoResponseLeaks() } @@ -247,7 +284,8 @@ internal class RetryingHttpClientTest { .willReturn(ok()) .willSetStateTo("COMPLETED") ) - val retryingClient = retryingHttpClientBuilder().maxRetries(1).build() + val sleeper = RecordingSleeper() + val retryingClient = retryingHttpClientBuilder(sleeper).maxRetries(1).build() val response = retryingClient.execute( @@ -261,6 +299,7 @@ internal class RetryingHttpClientTest { assertThat(response.statusCode()).isEqualTo(200) verify(2, postRequestedFor(urlPathEqualTo("/something"))) + assertThat(sleeper.durations).containsExactly(Duration.ofMillis(10)) assertNoResponseLeaks() } @@ -301,21 +340,12 @@ internal class RetryingHttpClientTest { override fun close() = httpClient.close() } + val sleeper = RecordingSleeper() val retryingClient = RetryingHttpClient.builder() .httpClient(failingHttpClient) .maxRetries(2) - .sleeper( - object : Sleeper { - - override fun sleep(duration: Duration) {} - - override fun sleepAsync(duration: Duration): CompletableFuture = - CompletableFuture.completedFuture(null) - - override fun close() {} - } - ) + .sleeper(sleeper) .build() val response = @@ -339,25 +369,153 @@ internal class RetryingHttpClientTest { postRequestedFor(urlPathEqualTo("/something")) .withHeader("x-stainless-retry-count", equalTo("0")), ) + // Exponential backoff with jitter: 0.5s * jitter where jitter is in [0.75, 1.0]. + assertThat(sleeper.durations).hasSize(1) + assertThat(sleeper.durations[0]).isBetween(Duration.ofMillis(375), Duration.ofMillis(500)) assertNoResponseLeaks() } - private fun retryingHttpClientBuilder() = - RetryingHttpClient.builder() - .httpClient(httpClient) - // Use a no-op `Sleeper` to make the test fast. - .sleeper( - object : Sleeper { + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun execute_withExponentialBackoff(async: Boolean) { + stubFor(post(urlPathEqualTo("/something")).willReturn(serviceUnavailable())) + val sleeper = RecordingSleeper() + val retryingClient = retryingHttpClientBuilder(sleeper).maxRetries(3).build() + + val response = + retryingClient.execute( + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(baseUrl) + .addPathSegment("something") + .build(), + async, + ) - override fun sleep(duration: Duration) {} + // All retries exhausted; the last 503 response is returned. + assertThat(response.statusCode()).isEqualTo(503) + verify(4, postRequestedFor(urlPathEqualTo("/something"))) + // Exponential backoff with jitter: backoff = min(0.5 * 2^(retries-1), 8) * jitter where + // jitter is in [0.75, 1.0]. + assertThat(sleeper.durations).hasSize(3) + // retries=1: 0.5s * [0.75, 1.0] + assertThat(sleeper.durations[0]).isBetween(Duration.ofMillis(375), Duration.ofMillis(500)) + // retries=2: 1.0s * [0.75, 1.0] + assertThat(sleeper.durations[1]).isBetween(Duration.ofMillis(750), Duration.ofMillis(1000)) + // retries=3: 2.0s * [0.75, 1.0] + assertThat(sleeper.durations[2]).isBetween(Duration.ofMillis(1500), Duration.ofMillis(2000)) + assertNoResponseLeaks() + } - override fun sleepAsync(duration: Duration): CompletableFuture = - CompletableFuture.completedFuture(null) + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun execute_withExponentialBackoffCap(async: Boolean) { + stubFor(post(urlPathEqualTo("/something")).willReturn(serviceUnavailable())) + val sleeper = RecordingSleeper() + val retryingClient = retryingHttpClientBuilder(sleeper).maxRetries(6).build() - override fun close() {} - } + val response = + retryingClient.execute( + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(baseUrl) + .addPathSegment("something") + .build(), + async, ) + assertThat(response.statusCode()).isEqualTo(503) + verify(7, postRequestedFor(urlPathEqualTo("/something"))) + assertThat(sleeper.durations).hasSize(6) + // retries=5: min(0.5 * 2^4, 8) = 8.0s * [0.75, 1.0] + assertThat(sleeper.durations[4]).isBetween(Duration.ofMillis(6000), Duration.ofMillis(8000)) + // retries=6: min(0.5 * 2^5, 8) = min(16, 8) = 8.0s * [0.75, 1.0] (capped) + assertThat(sleeper.durations[5]).isBetween(Duration.ofMillis(6000), Duration.ofMillis(8000)) + assertNoResponseLeaks() + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun execute_withRetryAfterMsPriorityOverRetryAfter(async: Boolean) { + stubFor( + post(urlPathEqualTo("/something")) + .inScenario("foo") + .whenScenarioStateIs(Scenario.STARTED) + .willReturn( + serviceUnavailable() + .withHeader("Retry-After-Ms", "50") + .withHeader("Retry-After", "2") + ) + .willSetStateTo("RETRY") + ) + stubFor( + post(urlPathEqualTo("/something")) + .inScenario("foo") + .whenScenarioStateIs("RETRY") + .willReturn(ok()) + .willSetStateTo("COMPLETED") + ) + val sleeper = RecordingSleeper() + val retryingClient = retryingHttpClientBuilder(sleeper).maxRetries(1).build() + + val response = + retryingClient.execute( + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(baseUrl) + .addPathSegment("something") + .build(), + async, + ) + + assertThat(response.statusCode()).isEqualTo(200) + // Retry-After-Ms (50ms) takes priority over Retry-After (2s). + assertThat(sleeper.durations).containsExactly(Duration.ofMillis(50)) + assertNoResponseLeaks() + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun execute_withRetryAfterUnparseable(async: Boolean) { + stubFor( + post(urlPathEqualTo("/something")) + .inScenario("foo") + .whenScenarioStateIs(Scenario.STARTED) + .willReturn(serviceUnavailable().withHeader("Retry-After", "not-a-date-or-number")) + .willSetStateTo("RETRY") + ) + stubFor( + post(urlPathEqualTo("/something")) + .inScenario("foo") + .whenScenarioStateIs("RETRY") + .willReturn(ok()) + .willSetStateTo("COMPLETED") + ) + val sleeper = RecordingSleeper() + val retryingClient = retryingHttpClientBuilder(sleeper).maxRetries(1).build() + + val response = + retryingClient.execute( + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(baseUrl) + .addPathSegment("something") + .build(), + async, + ) + + assertThat(response.statusCode()).isEqualTo(200) + // Unparseable Retry-After falls through to exponential backoff. + assertThat(sleeper.durations).hasSize(1) + assertThat(sleeper.durations[0]).isBetween(Duration.ofMillis(375), Duration.ofMillis(500)) + assertNoResponseLeaks() + } + + private fun retryingHttpClientBuilder( + sleeper: RecordingSleeper, + clock: Clock = Clock.systemUTC(), + ) = RetryingHttpClient.builder().httpClient(httpClient).sleeper(sleeper).clock(clock) + private fun HttpClient.execute(request: HttpRequest, async: Boolean): HttpResponse = if (async) executeAsync(request).get() else execute(request) diff --git a/brand-dev-java-core/src/test/kotlin/com/branddev/api/models/brand/BrandRetrieveByNameParamsTest.kt b/brand-dev-java-core/src/test/kotlin/com/branddev/api/models/brand/BrandRetrieveByNameParamsTest.kt index bd3d663..9071f7c 100644 --- a/brand-dev-java-core/src/test/kotlin/com/branddev/api/models/brand/BrandRetrieveByNameParamsTest.kt +++ b/brand-dev-java-core/src/test/kotlin/com/branddev/api/models/brand/BrandRetrieveByNameParamsTest.kt @@ -12,6 +12,7 @@ internal class BrandRetrieveByNameParamsTest { fun create() { BrandRetrieveByNameParams.builder() .name("xxx") + .countryGl(BrandRetrieveByNameParams.CountryGl.AD) .forceLanguage(BrandRetrieveByNameParams.ForceLanguage.ALBANIAN) .maxSpeed(true) .timeoutMs(1000L) @@ -23,6 +24,7 @@ internal class BrandRetrieveByNameParamsTest { val params = BrandRetrieveByNameParams.builder() .name("xxx") + .countryGl(BrandRetrieveByNameParams.CountryGl.AD) .forceLanguage(BrandRetrieveByNameParams.ForceLanguage.ALBANIAN) .maxSpeed(true) .timeoutMs(1000L) @@ -34,6 +36,7 @@ internal class BrandRetrieveByNameParamsTest { .isEqualTo( QueryParams.builder() .put("name", "xxx") + .put("country_gl", "ad") .put("force_language", "albanian") .put("maxSpeed", "true") .put("timeoutMS", "1000") diff --git a/brand-dev-java-core/src/test/kotlin/com/branddev/api/services/async/BrandServiceAsyncTest.kt b/brand-dev-java-core/src/test/kotlin/com/branddev/api/services/async/BrandServiceAsyncTest.kt index 92f995f..a270db9 100644 --- a/brand-dev-java-core/src/test/kotlin/com/branddev/api/services/async/BrandServiceAsyncTest.kt +++ b/brand-dev-java-core/src/test/kotlin/com/branddev/api/services/async/BrandServiceAsyncTest.kt @@ -260,6 +260,7 @@ internal class BrandServiceAsyncTest { brandServiceAsync.retrieveByName( BrandRetrieveByNameParams.builder() .name("xxx") + .countryGl(BrandRetrieveByNameParams.CountryGl.AD) .forceLanguage(BrandRetrieveByNameParams.ForceLanguage.ALBANIAN) .maxSpeed(true) .timeoutMs(1000L) diff --git a/brand-dev-java-core/src/test/kotlin/com/branddev/api/services/blocking/BrandServiceTest.kt b/brand-dev-java-core/src/test/kotlin/com/branddev/api/services/blocking/BrandServiceTest.kt index 76ba299..b9d402d 100644 --- a/brand-dev-java-core/src/test/kotlin/com/branddev/api/services/blocking/BrandServiceTest.kt +++ b/brand-dev-java-core/src/test/kotlin/com/branddev/api/services/blocking/BrandServiceTest.kt @@ -248,6 +248,7 @@ internal class BrandServiceTest { brandService.retrieveByName( BrandRetrieveByNameParams.builder() .name("xxx") + .countryGl(BrandRetrieveByNameParams.CountryGl.AD) .forceLanguage(BrandRetrieveByNameParams.ForceLanguage.ALBANIAN) .maxSpeed(true) .timeoutMs(1000L) diff --git a/build.gradle.kts b/build.gradle.kts index f4b8e31..259a006 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,7 +9,7 @@ repositories { allprojects { group = "com.branddev.api" - version = "0.1.0-alpha.35" // x-release-please-version + version = "0.1.0-alpha.36" // x-release-please-version } subprojects {