Skip to content

Commit

Permalink
Merge pull request #69 from Automattic/hamorillo/65-openapi-generation
Browse files Browse the repository at this point in the history
SDK - APIs code generation based on OpenApi definition
  • Loading branch information
hamorillo authored Mar 18, 2024
2 parents a87bc13 + 616237b commit ce54775
Show file tree
Hide file tree
Showing 28 changed files with 788 additions and 116 deletions.
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,33 @@ By default, **Android Studio** will show a warning, `Redundant visibility modifi
You can remove the warning by changing the setting for this project
in: `Settings` -> `Editor` -> `Inspections` -> `Kotlin` -> `Redundant Constructors` -> `Redundant visibility modifier`.

## Generating API code from OpenApi definitions

We use [OpenAPI Generator](https://openapi-generator.tech/) to generate the API code from the OpenAPI definitions.

The SDK project has integrated the OpenAPI Generator Gradle plugin to generate the API code from the OpenAPI definitions. The plugin is
configured in the `build.gradle.kts` file.

The OpenAPI definitions are located in the `openapi` directory. In the same directory, you can find the `templates` directory, which contains
the custom templates used by the OpenAPI Generator to generate the code that the Gravatar library needs. You can obtain the default templates by running the following command:

```sh
openapi-generator author template -g kotlin --library jvm-retrofit2
```

The [OpenAPI Generator documentation](https://openapi-generator.tech/docs/templating) provides more information about the templates.

The generator's output folder is the `build` directory. However, as we don't need all the generated files, the Gradle task has been modified to move only the desired code to the `gravatar` module. In addition, the
last step of the task is to format the generated code with [Ktlint](README.md#coding-style).

<span style="color:red">**Important:**</span> Do not manually modify the `com.gravatar.api` folder. The OpenAPI Generator will overwrite it.

To regerate the code you can use the following gradlew task:

```sh
./gradlew :gravatar:openApiGenerate
```

## Publishing

The SDK is published to the Automattic's S3 instance via [`publish-to-s3`](https://github.com/Automattic/publish-to-s3-gradle-plugin) Gradle plugin.
Expand Down
6 changes: 3 additions & 3 deletions app/src/main/java/com/gravatar/demoapp/ui/DemoGravatarApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ import com.gravatar.DefaultAvatarImage
import com.gravatar.GravatarApi
import com.gravatar.ImageRating
import com.gravatar.R
import com.gravatar.api.models.UserProfiles
import com.gravatar.demoapp.theme.GravatarDemoAppTheme
import com.gravatar.demoapp.ui.components.GravatarEmailInput
import com.gravatar.demoapp.ui.components.ProfileCard
import com.gravatar.demoapp.ui.model.SettingsState
import com.gravatar.emailAddressToGravatarUrl
import com.gravatar.models.UserProfiles
import com.gravatar.sha256Hash
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -146,7 +146,7 @@ private fun GravatarTabs(
private fun ProfileTab(modifier: Modifier = Modifier, onError: (String?, Throwable?) -> Unit) {
var email by remember { mutableStateOf("[email protected]", neverEqualPolicy()) }
var hash by remember { mutableStateOf("", neverEqualPolicy()) }
var profiles by remember { mutableStateOf(UserProfiles(), neverEqualPolicy()) }
var profiles by remember { mutableStateOf(UserProfiles(emptyList()), neverEqualPolicy()) }
var loading by remember { mutableStateOf(false) }
var error by remember { mutableStateOf("") }
val gravatarApi = GravatarApi()
Expand Down Expand Up @@ -195,7 +195,7 @@ private fun ProfileTab(modifier: Modifier = Modifier, onError: (String?, Throwab
}
}
// Show the profile card if we got a result and there is no error and it's not loading
if (!loading && error.isEmpty() && profiles.entry.size > 0) {
if (!loading && error.isEmpty() && profiles.entry.isNotEmpty()) {
ProfileCard(
profiles.entry.first(),
Modifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastFirstOrNull
import coil.compose.AsyncImage
import com.gravatar.api.models.Email
import com.gravatar.api.models.UserProfile
import com.gravatar.gravatarUrl
import com.gravatar.models.Email
import com.gravatar.models.UserProfile

@Composable
fun ProfileCard(profile: UserProfile, modifier: Modifier = Modifier, avatarImageSize: Dp = 128.dp) {
Expand Down Expand Up @@ -78,7 +78,7 @@ fun ProfileCard(profile: UserProfile, modifier: Modifier = Modifier, avatarImage
) { }

// Find primary email
val primaryEmail = profile.emails.fastFirstOrNull { it.primary ?: false }?.value
val primaryEmail = profile.emails?.fastFirstOrNull { it.primary ?: false }?.value

primaryEmail?.let {
Button(
Expand All @@ -103,6 +103,7 @@ fun ProfileCard(profile: UserProfile, modifier: Modifier = Modifier, avatarImage
fun PreviewUserProfileCard() {
ProfileCard(
UserProfile(
hash = "1234567890",
displayName = "John Doe",
preferredUsername = "johndoe",
aboutMe = "I'm a farmer who loves to code",
Expand Down
50 changes: 50 additions & 0 deletions gravatar/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ plugins {

// Dokka
id("org.jetbrains.dokka") version "1.9.10"

// OpenApi Generator
id("org.openapi.generator") version "7.4.0"
}

android {
Expand Down Expand Up @@ -110,3 +113,50 @@ project.afterEvaluate {
}
}
}

openApiGenerate {
generatorName = "kotlin"
inputSpec = "${projectDir.path}/openapi/api-gravatar.yaml"
outputDir = "${buildDir.path}/openapi"

// Use the custom templates if they are present. If not, the generator will use the default ones
templateDir.set("${projectDir.path}/openapi/templates")

// Set the generation configuration options
configOptions.set(
mapOf(
"library" to "jvm-retrofit2",
"serializationLibrary" to "gson",
"groupId" to "com.gravatar",
"packageName" to "com.gravatar.api",
),
)

// We only want the apis and models, not the "infrastructure" folder
// See: https://github.com/OpenAPITools/openapi-generator/issues/6455
globalProperties.set(
mapOf(
"apis" to "",
"models" to "",
),
)
}

tasks.openApiGenerate {
// Workaround for avoid the build error
notCompatibleWithConfigurationCache("Incomplete support for configuration cache in OpenAPI Generator plugin.")

// Move the generated code to the correct package and remove the generated folder
doLast {
file("${projectDir.path}/src/main/java/com/gravatar/api").deleteRecursively()
file("${buildDir.path}/openapi/src/main/kotlin/com/gravatar/api")
.renameTo(file("${projectDir.path}/src/main/java/com/gravatar/api"))
file("${buildDir.path}/openapi").deleteRecursively()
}

// Format the generated code
this.finalizedBy(tasks.ktlintFormat.get().path)

// Always run the task forcing the up-to-date check to return false
outputs.upToDateWhen { false }
}
132 changes: 132 additions & 0 deletions gravatar/openapi/api-gravatar.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
openapi: "3.1.0"
info:
version: 1.0.0
title: Gravatar
servers:
- url: https://www.gravatar.com
paths:
/{hash}.json:
get:
summary: Profile for a specific hash
operationId: getProfile
tags:
- profile
parameters:
- name: hash
in: path
required: true
description: The email's hash of the profile to retrieve.
schema:
type: string
responses:
'200':
description: Expected response to a valid request
content:
application/json:
schema:
$ref: "#/components/schemas/UserProfiles"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
components:
schemas:
UserProfiles:
type: object
required:
- entry
properties:
entry:
type: array
default: []
items:
$ref: "#/components/schemas/UserProfile"
UserProfile:
type: object
required:
- hash
properties:
hash:
description: The email's hash of the profile.
type: string
requestHash:
type: string
profileUrl:
type: string
format: url
preferredUsername:
type: string
thumbnailUrl:
type: string
format: url
last_profile_edit:
type: string
profileBackground:
$ref: "#/components/schemas/ProfileBackground"
name:
$ref: "#/components/schemas/Name"
displayName:
type: string
pronouns:
type: string
aboutMe:
type: string
currentLocation:
type: string
share_flags:
$ref: "#/components/schemas/ShareFlags"
emails:
type: array
items:
$ref: "#/components/schemas/Email"
Name:
type: object
required:
- givenName
- familyName
- formatted
properties:
givenName:
type: string
familyName:
type: string
formatted:
type: string
ShareFlags:
type: object
required:
- search_engines
properties:
search_engines:
type: boolean
ProfileBackground:
type: object
required:
- color
- url
properties:
color:
type: string
url:
type: string
format: url
Email:
type: object
properties:
primary:
type: boolean
value:
type: string
Error:
type: object
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string
Loading

0 comments on commit ce54775

Please sign in to comment.