Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import com.avsystem.justworks.core.parser.SpecParser
import com.squareup.kotlinpoet.FileSpec
import java.io.File
import kotlin.test.Test
import kotlin.test.assertContains
import kotlin.test.assertFalse
import kotlin.test.assertIs
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlin.test.fail
Expand All @@ -28,6 +30,13 @@ class IntegrationTest {
"/fixtures/platform-api.json",
"/fixtures/analytics-api.json",
)

/** Vendored popular public OpenAPI specs (see fixtures/public/SOURCES.md). */
private val PUBLIC_SPECS = listOf(
"/fixtures/public/swagger-petstore.json",
"/fixtures/public/petstore-expanded.yaml",
"/fixtures/public/uspto.yaml",
)
}

private fun parseSpec(resourcePath: String): ParseResult.Success<ApiSpec> {
Expand Down Expand Up @@ -209,4 +218,129 @@ class IntegrationTest {
}
}
}

// -- Popular public OpenAPI specs (issue #47) --

/** All generated sources for a fixture, indexed by KotlinPoet file name. */
private class Generated(val modelByName: Map<String, String>, val clientByName: Map<String, String>,) {
fun model(name: String): String =
modelByName[name] ?: fail("Expected model file '$name', available: ${modelByName.keys}")

fun client(name: String): String =
clientByName[name] ?: fail("Expected client file '$name', available: ${clientByName.keys}")
}

private fun generateAll(fixture: String): Generated {
val spec = parseSpec(fixture).value
val (modelFiles, resolvedSpec) = generateModelWithResolvedSpec(spec)
val clientFiles = if (spec.endpoints.isNotEmpty()) generateClient(resolvedSpec) else emptyList()
return Generated(
modelFiles.associate { it.name to it.toString() },
clientFiles.associate { it.name to it.toString() },
)
}

@Test
fun `public specs parse without errors`() {
for (fixture in PUBLIC_SPECS) {
val specUrl = javaClass.getResource(fixture) ?: fail("Public spec fixture not found: $fixture")
val result = SpecParser.parse(File(specUrl.toURI()))

val failureError = (result as? ParseResult.Failure)?.error
assertIs<ParseResult.Success<*>>(
result,
"$fixture should parse successfully, but failed: $failureError",
)

// Known limitations surface as warnings, never silent failures or hard errors.
if (result.warnings.isNotEmpty()) {
println("$fixture parsed with ${result.warnings.size} warning(s):")
result.warnings.forEach { println(" - ${it.message}") }
}
}
}

@Test
fun `swagger-petstore generates expected model and client code`() {
val gen = generateAll("/fixtures/public/swagger-petstore.json")

// Pet model: required props non-null, optional props nullable with defaults,
// object refs become typed references, @SerialName preserves wire names.
val pet = gen.model("Pet")
assertContains(pet, "public data class Pet(")
assertContains(pet, "public val name: String,") // required -> non-null, no default
assertContains(pet, "public val photoUrls: List<String>,") // required array -> non-null List
assertContains(pet, "public val id: Long? = null") // optional -> nullable with default
assertContains(pet, "public val category: Category? = null") // $ref -> typed reference
assertContains(pet, "public val tags: List<Tag>? = null") // array of $ref
assertContains(pet, """@SerialName("photoUrls")""")

// StoreApi.getInventory: object with additionalProperties: integer -> Map<String, Int>.
val storeApi = gen.client("StoreApi")
assertContains(
storeApi,
"public suspend fun getInventory(): HttpResult<JsonElement, Map<String, Int>>",
)

// PetApi.getPetById: int64 path param -> Long, path templated via encodeParam.
val petApi = gen.client("PetApi")
assertContains(
petApi,
"public suspend fun getPetById(petId: Long): HttpResult<JsonElement, Pet>",
)
assertContains(petApi, "/pet/\${encodeParam(petId)}")
}

@Test
fun `petstore-expanded generates expected model and client code`() {
val gen = generateAll("/fixtures/public/petstore-expanded.yaml")

// Pet uses allOf(NewPet, {id}) -> flattened: name (from NewPet, required),
// tag (from NewPet, optional), id (required in the inline part).
val pet = gen.model("Pet")
assertContains(pet, "public data class Pet(")
assertContains(pet, "public val name: String,")
assertContains(pet, "public val id: Long,") // required -> non-null
assertContains(pet, "public val tag: String? = null")

val error = gen.model("Error")
assertContains(error, "public val code: Int,")
assertContains(error, "public val message: String,")

val api = gen.client("DefaultApi")
// Optional query params -> nullable with default, appended only when non-null.
assertContains(
api,
"public suspend fun findPets(tags: List<String>? = null, limit: Int? = null): " +
"HttpResult<JsonElement, List<Pet>>",
)
// requestBody $ref -> typed body param + JSON content type + setBody.
assertContains(api, "public suspend fun addPet(body: NewPet): HttpResult<JsonElement, Pet>")
assertContains(api, "contentType(ContentType.Application.Json)")
assertContains(api, "setBody(body)")
// 204 No Content response -> Unit result via toEmptyResult().
assertContains(api, "public suspend fun deletePet(id: Long): HttpResult<JsonElement, Unit>")
assertContains(api, ".toEmptyResult()")
}

@Test
fun `uspto generates expected model and degrades freeform response`() {
val gen = generateAll("/fixtures/public/uspto.yaml")

// Nested inline object array item gets a synthesized name.
val dataSetList = gen.model("dataSetList")
assertContains(dataSetList, "public val total: Int? = null")
assertContains(dataSetList, "public val apis: List<dataSetList_ApisItem>? = null")

val metadataApi = gen.client("MetadataApi")
assertContains(
metadataApi,
"public suspend fun listDataSets(): HttpResult<JsonElement, dataSetList>",
)

// Freeform search response (no schema) degrades to JsonElement, never a hard failure.
val searchApi = gen.client("SearchApi")
assertContains(searchApi, "import kotlinx.serialization.json.JsonElement")
assertContains(searchApi, "public suspend fun performSearch(")
}
}
7 changes: 7 additions & 0 deletions core/src/test/resources/fixtures/public/SOURCES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Vendored public OpenAPI specs (for integration tests)

- `swagger-petstore.json` — Swagger Petstore (OpenAPI 3.0.4), https://petstore3.swagger.io/api/v3/openapi.json
- `petstore-expanded.yaml` — OAI example (uses allOf), https://github.com/OAI/OpenAPI-Specification (tag 3.0.4, examples/v3.0/petstore-expanded.yaml). The upstream `NewPet.tag` type typo (`sthciring`) was corrected to `string`.
- `uspto.yaml` — OAI example, real USPTO Data Set API, https://github.com/OAI/OpenAPI-Specification (examples/v3.0/uspto.yaml)

Vendored (not fetched at build time) for deterministic, offline tests.
158 changes: 158 additions & 0 deletions core/src/test/resources/fixtures/public/petstore-expanded.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
openapi: "3.0.0"
info:
version: 1.0.0
title: Swagger Petstore
description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification
termsOfService: http://swagger.io/terms/
contact:
name: Swagger API Team
email: apiteam@swagger.io
url: http://swagger.io
license:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0.html
servers:
- url: https://petstore.swagger.io/v2
paths:
/pets:
get:
description: |
Returns all pets from the system that the user has access to
Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia.

Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien.
operationId: findPets
parameters:
- name: tags
in: query
description: tags to filter by
required: false
style: form
schema:
type: array
items:
type: string
- name: limit
in: query
description: maximum number of results to return
required: false
schema:
type: integer
format: int32
responses:
'200':
description: pet response
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
post:
description: Creates a new pet in the store. Duplicates are allowed
operationId: addPet
requestBody:
description: Pet to add to the store
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NewPet'
responses:
'200':
description: pet response
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/pets/{id}:
get:
description: Returns a user based on a single ID, if the user does not have access to the pet
operationId: find pet by id
parameters:
- name: id
in: path
description: ID of pet to fetch
required: true
schema:
type: integer
format: int64
responses:
'200':
description: pet response
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
delete:
description: deletes a single pet based on the ID supplied
operationId: deletePet
parameters:
- name: id
in: path
description: ID of pet to delete
required: true
schema:
type: integer
format: int64
responses:
'204':
description: pet deleted
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
Pet:
allOf:
- $ref: '#/components/schemas/NewPet'
- type: object
required:
- id
properties:
id:
type: integer
format: int64

NewPet:
type: object
required:
- name
properties:
name:
type: string
tag:
type: string

Error:
type: object
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string
Loading
Loading