Skip to content

Commit e8c8b2d

Browse files
authored
chore: customize generator templates (#3)
Add generator templates customized for XAP Add lodging and hotel specs
1 parent ce60ff5 commit e8c8b2d

File tree

35 files changed

+5571
-10
lines changed

35 files changed

+5571
-10
lines changed

.github/workflows/generate-and-publish-sdk-sources.yaml

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,14 @@ on:
99
E.g., 1.0.0, 1.0.1, 1.0.0-SNAPSHOT, etc.
1010
required: true
1111
type: string
12-
sdk_repo_ref:
13-
description: |
14-
Branch or tag to checkout on the `expediagroup-java-sdk` repository.
15-
Leave empty to use the default branch.
16-
type: string
17-
default: ''
1812

1913
jobs:
2014
generate-and-publish-sources:
21-
uses: ExpediaGroup/expediagroup-java-sdk/.github/workflows/selfserve-full-workflow.yaml@main
15+
uses: ExpediaGroup/expediagroup-java-sdk/.github/workflows/selfserve-full-workflow.yaml@v20241013
2216
secrets: inherit
2317
with:
2418
name: xap
2519
version: ${{ inputs.version }}
26-
transformations: "-th --operationIdsToTags"
20+
transformations: "--headers key --operationIdsToTags"
2721
repository: 'ExpediaGroup/xap-java-sdk'
28-
sdk_repo_ref: ${{ inputs.sdk_repo_ref }}
22+
ref: ${{ github.head_ref || github.ref_name }}

.github/workflows/release-sdk.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ on:
1212

1313
jobs:
1414
release-sdk:
15-
uses: ExpediaGroup/expediagroup-java-sdk/.github/workflows/selfserve-release-sdk.yaml@main
15+
uses: ExpediaGroup/expediagroup-java-sdk/.github/workflows/selfserve-release-sdk.yaml@v20241013
1616
secrets: inherit
1717
with:
1818
branch: ${{ inputs.branch }}
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
/*
2+
* Copyright (C) 2022 Expedia, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.expediagroup.sdk.core.plugin.serialization
17+
18+
import com.expediagroup.sdk.core.plugin.logging.ExpediaGroupLoggerFactory
19+
import io.ktor.client.HttpClient
20+
import io.ktor.client.plugins.HttpClientPlugin
21+
import io.ktor.client.plugins.contentnegotiation.ContentConverterException
22+
import io.ktor.client.plugins.contentnegotiation.JsonContentTypeMatcher
23+
import io.ktor.client.request.HttpRequestBuilder
24+
import io.ktor.client.request.HttpRequestPipeline
25+
import io.ktor.client.statement.HttpResponseContainer
26+
import io.ktor.client.statement.HttpResponsePipeline
27+
import io.ktor.client.utils.EmptyContent
28+
import io.ktor.http.ContentType
29+
import io.ktor.http.ContentTypeMatcher
30+
import io.ktor.http.HttpHeaders
31+
import io.ktor.http.HttpStatusCode
32+
import io.ktor.http.Url
33+
import io.ktor.http.charset
34+
import io.ktor.http.content.NullBody
35+
import io.ktor.http.content.OutgoingContent
36+
import io.ktor.http.contentType
37+
import io.ktor.serialization.Configuration
38+
import io.ktor.serialization.ContentConverter
39+
import io.ktor.serialization.deserialize
40+
import io.ktor.serialization.suitableCharset
41+
import io.ktor.util.AttributeKey
42+
import io.ktor.util.InternalAPI
43+
import io.ktor.util.reflect.TypeInfo
44+
import io.ktor.utils.io.ByteReadChannel
45+
import io.ktor.utils.io.charsets.Charset
46+
import java.io.InputStream
47+
import kotlin.reflect.KClass
48+
49+
internal val DefaultCommonIgnoredTypes: Set<KClass<*>> =
50+
setOf(
51+
ByteArray::class,
52+
String::class,
53+
HttpStatusCode::class,
54+
ByteReadChannel::class,
55+
OutgoingContent::class
56+
)
57+
58+
internal val DefaultIgnoredTypes: Set<KClass<*>> = mutableSetOf(InputStream::class)
59+
60+
internal class ContentNegotiation(
61+
private val registrations: List<Config.ConverterRegistration>,
62+
private val ignoredTypes: Set<KClass<*>>
63+
) {
64+
internal class Config : Configuration {
65+
internal class ConverterRegistration(
66+
val converter: ContentConverter,
67+
val contentTypeToSend: ContentType,
68+
val contentTypeMatcher: ContentTypeMatcher
69+
)
70+
71+
val registrations = mutableListOf<ConverterRegistration>()
72+
val ignoredTypes: MutableSet<KClass<*>> =
73+
(DefaultIgnoredTypes + DefaultCommonIgnoredTypes).toMutableSet()
74+
75+
override fun <T : ContentConverter> register(
76+
contentType: ContentType,
77+
converter: T,
78+
configuration: T.() -> Unit
79+
) {
80+
val matcher =
81+
when (contentType) {
82+
ContentType.Application.Json -> JsonContentTypeMatcher
83+
else -> defaultMatcher(contentType)
84+
}
85+
register(contentType, converter, matcher, configuration)
86+
}
87+
88+
private fun <T : ContentConverter> register(
89+
contentTypeToSend: ContentType,
90+
converter: T,
91+
contentTypeMatcher: ContentTypeMatcher,
92+
configuration: T.() -> Unit
93+
) {
94+
val registration =
95+
ConverterRegistration(
96+
converter.apply(configuration),
97+
contentTypeToSend,
98+
contentTypeMatcher
99+
)
100+
registrations.add(registration)
101+
}
102+
103+
private fun defaultMatcher(pattern: ContentType): ContentTypeMatcher =
104+
object : ContentTypeMatcher {
105+
override fun contains(contentType: ContentType): Boolean = contentType.match(pattern)
106+
}
107+
}
108+
109+
private val log = ExpediaGroupLoggerFactory.getLogger(this::class.java)
110+
111+
internal suspend fun convertRequest(
112+
request: HttpRequestBuilder,
113+
body: Any
114+
): Any? {
115+
if (body is OutgoingContent || ignoredTypes.any { it.isInstance(body) }) {
116+
log.trace(
117+
"Body type ${body::class} is in ignored types. " +
118+
"Skipping ContentNegotiation for ${request.url}."
119+
)
120+
return null
121+
}
122+
val contentType =
123+
request.contentType() ?: run {
124+
log.trace("Request doesn't have Content-Type header. Skipping ContentNegotiation for ${request.url}.")
125+
return null
126+
}
127+
128+
if (body is Unit) {
129+
log.trace("Sending empty body for ${request.url}")
130+
request.headers.remove(HttpHeaders.ContentType)
131+
return EmptyContent
132+
}
133+
134+
val matchingRegistrations =
135+
registrations.filter { it.contentTypeMatcher.contains(contentType) }
136+
.takeIf { it.isNotEmpty() } ?: run {
137+
log.trace(
138+
"None of the registered converters match request Content-Type=$contentType. " +
139+
"Skipping ContentNegotiation for ${request.url}."
140+
)
141+
return null
142+
}
143+
if (request.bodyType == null) {
144+
log.trace("Request has unknown body type. Skipping ContentNegotiation for ${request.url}.")
145+
return null
146+
}
147+
request.headers.remove(HttpHeaders.ContentType)
148+
149+
// Pick the first one that can convert the subject successfully
150+
val serializedContent =
151+
matchingRegistrations.firstNotNullOfOrNull { registration ->
152+
val result =
153+
registration.converter.serializeNullable(
154+
contentType,
155+
contentType.charset() ?: Charsets.UTF_8,
156+
request.bodyType!!,
157+
body.takeIf { it != NullBody }
158+
)
159+
if (result != null) {
160+
log.trace("Converted request body using ${registration.converter} for ${request.url}")
161+
}
162+
result
163+
} ?: throw ContentConverterException(
164+
"Can't convert $body with contentType $contentType using converters " +
165+
matchingRegistrations.joinToString { it.converter.toString() }
166+
)
167+
168+
return serializedContent
169+
}
170+
171+
@OptIn(InternalAPI::class)
172+
internal suspend fun convertResponse(
173+
requestUrl: Url,
174+
info: TypeInfo,
175+
body: Any,
176+
responseContentType: ContentType,
177+
charset: Charset = Charsets.UTF_8
178+
): Any? {
179+
if (body !is ByteReadChannel) {
180+
log.trace("Response body is already transformed. Skipping ContentNegotiation for $requestUrl.")
181+
return null
182+
}
183+
if (info.type in ignoredTypes) {
184+
log.trace(
185+
"Response body type ${info.type} is in ignored types. " +
186+
"Skipping ContentNegotiation for $requestUrl."
187+
)
188+
return null
189+
}
190+
191+
log.debug("Test: ${registrations.size}")
192+
val suitableConverters =
193+
registrations
194+
.filter { it.contentTypeMatcher.contains(responseContentType) }
195+
.map { it.converter }
196+
.takeIf { it.isNotEmpty() }
197+
?: run {
198+
log.trace(
199+
"None of the registered converters match response with Content-Type=$responseContentType. " +
200+
"Skipping ContentNegotiation for $requestUrl."
201+
)
202+
return null
203+
}
204+
205+
val result = suitableConverters.deserialize(body, info, charset)
206+
if (result !is ByteReadChannel) {
207+
log.trace("Response body was converted to ${result::class} for $requestUrl.")
208+
}
209+
return result
210+
}
211+
212+
companion object Plugin : HttpClientPlugin<Config, ContentNegotiation> {
213+
override val key: AttributeKey<ContentNegotiation> = AttributeKey("ContentNegotiation")
214+
private val log = ExpediaGroupLoggerFactory.getLogger(this::class.java)
215+
216+
override fun install(
217+
plugin: ContentNegotiation,
218+
scope: HttpClient
219+
) {
220+
scope.requestPipeline.intercept(HttpRequestPipeline.Transform) {
221+
val result = plugin.convertRequest(context, subject) ?: return@intercept
222+
proceedWith(result)
223+
}
224+
225+
scope.responsePipeline.intercept(HttpResponsePipeline.Transform) { (info, body) ->
226+
val contentType =
227+
context.response.contentType() ?: run {
228+
log.trace("Response doesn't have \"Content-Type\" header, skipping ContentNegotiation plugin")
229+
return@intercept
230+
}
231+
val charset = context.request.headers.suitableCharset()
232+
233+
val deserializedBody =
234+
plugin.convertResponse(context.request.url, info, body, contentType, charset)
235+
?: return@intercept
236+
val result = HttpResponseContainer(info, deserializedBody)
237+
proceedWith(result)
238+
}
239+
}
240+
241+
override fun prepare(block: Config.() -> Unit): ContentNegotiation {
242+
val config = Config().apply(block)
243+
return ContentNegotiation(config.registrations, config.ignoredTypes)
244+
}
245+
}
246+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright (C) 2022 Expedia, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.expediagroup.sdk.core.plugin.serialization
17+
18+
import com.expediagroup.sdk.core.client.Client
19+
import com.expediagroup.sdk.core.plugin.Plugin
20+
import com.fasterxml.jackson.databind.DeserializationFeature
21+
import com.fasterxml.jackson.databind.PropertyNamingStrategies
22+
import io.ktor.serialization.jackson.jackson
23+
import java.text.SimpleDateFormat
24+
25+
internal object SerializationPlugin : Plugin<SerializationConfiguration> {
26+
override fun install(
27+
client: Client,
28+
configurations: SerializationConfiguration
29+
) {
30+
configurations.httpClientConfiguration.install(ContentNegotiation) {
31+
jackson {
32+
enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)
33+
disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
34+
setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
35+
setDateFormat(SimpleDateFormat())
36+
findAndRegisterModules()
37+
}
38+
}
39+
}
40+
}

0 commit comments

Comments
 (0)