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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ data class OpenAPISpecLoaderCfg(
@JsonDeserialize(using = WidgetSerde.MultiSelectDeserializer::class)
@JsonSerialize(using = WidgetSerde.MultiSelectSerializer::class)
@JsonProperty("connection_qualified_name") val connectionQualifiedName: List<String>? = null,
@JsonProperty("object_creation_mode") val objectCreationMode: String = "PHYSICAL",
) : CustomConfig<OpenAPISpecLoaderCfg>()
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,21 @@ uiConfig {
}
}
}
["Options"] {
description = "Asset creation options"
inputs {
["object_creation_mode"] = new Radio {
title = "Schema object creation"
required = true
possibleValues {
["PHYSICAL"] = "Physical instances (per-body, enables field-level lineage)"
["NONE"] = "None (bodies-only, rely on JSON blobs on API Methods)"
}
default = "PHYSICAL"
helpText = "Physical creates separate API Object/Field assets for each request/response body occurrence. None skips object/field creation entirely to reduce asset count."
}
}
}
["Connection"] {
description = "Connection details"
inputs {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/* SPDX-License-Identifier: Apache-2.0
Copyright 2023 Atlan Pte. Ltd. */
import com.atlan.model.assets.APIField
import com.atlan.model.assets.APIMethod
import com.atlan.model.assets.APIObject
import com.atlan.model.assets.APIPath
import com.atlan.model.assets.APISpec
import com.atlan.model.assets.Connection
Expand Down Expand Up @@ -116,6 +119,179 @@ class ImportJsonTest : PackageTest("j") {
}
}

@Test
fun objectsCreated() {
// The Petstore spec has 8 schemas: Order, Customer, Address, Category, User, Tag, Pet, ApiResponse
val connectionQN = Connection.findByName(client, testId, connectorType)?.get(0)?.qualifiedName!!
val request =
APIObject
.select(client)
.where(APIObject.QUALIFIED_NAME.startsWith(connectionQN))
.includeOnResults(APIObject.NAME)
.includeOnResults(APIObject.API_FIELD_COUNT)
.includeOnResults(APIObject.API_SPEC_QUALIFIED_NAME)
.toRequest()
val response = retrySearchUntil(request, 8)
val results = response.stream().toList()
assertEquals(8, results.size)
val objectNames = results.map { (it as APIObject).name }.toSet()
assertTrue(objectNames.contains("Pet"))
assertTrue(objectNames.contains("Order"))
assertTrue(objectNames.contains("User"))
assertTrue(objectNames.contains("Category"))
assertTrue(objectNames.contains("Tag"))
assertTrue(objectNames.contains("Address"))
assertTrue(objectNames.contains("Customer"))
assertTrue(objectNames.contains("ApiResponse"))
// Verify Pet has the correct field count (6 properties: id, name, category, photoUrls, tags, status)
val pet = results.first { (it as APIObject).name == "Pet" } as APIObject
assertEquals(6L, pet.apiFieldCount)
}

@Test
fun fieldsCreated() {
// Verify APIField assets were created for schema properties
val connectionQN = Connection.findByName(client, testId, connectorType)?.get(0)?.qualifiedName!!
val request =
APIField
.select(client)
.where(APIField.QUALIFIED_NAME.startsWith(connectionQN))
.includeOnResults(APIField.NAME)
.includeOnResults(APIField.API_FIELD_TYPE)
.includeOnResults(APIField.API_IS_OBJECT_REFERENCE)
.includeOnResults(APIField.API_OBJECT_QUALIFIED_NAME)
.includeOnResults(APIField.API_OBJECT)
.toRequest()
val response = retrySearchUntil(request, 1)
val results = response.stream().toList()
// There should be fields across all schemas
assertTrue(results.isNotEmpty())
// Find a field that is an object reference (e.g., Pet.category -> Category)
val objectRefFields = results.filter { (it as APIField).apiIsObjectReference == true }
assertTrue(objectRefFields.isNotEmpty(), "Should have at least one field that references another APIObject")
// Every field should have a parent APIObject
results.forEach {
val field = it as APIField
assertFalse(field.name.isNullOrBlank())
assertFalse(field.apiFieldType.isNullOrBlank())
}
}

@Test
fun methodsCreated() {
// The Petstore spec has 20 operations total across all paths
val connectionQN = Connection.findByName(client, testId, connectorType)?.get(0)?.qualifiedName!!
val request =
APIMethod
.select(client)
.where(APIMethod.QUALIFIED_NAME.startsWith(connectionQN))
.includeOnResults(APIMethod.NAME)
.includeOnResults(APIMethod.DESCRIPTION)
.includeOnResults(APIMethod.API_METHOD_REQUEST)
.includeOnResults(APIMethod.API_METHOD_RESPONSE)
.includeOnResults(APIMethod.API_METHOD_RESPONSE_CODES)
.includeOnResults(APIMethod.API_PATH)
.includeOnRelations(APIPath.QUALIFIED_NAME)
.toRequest()
val response = retrySearchUntil(request, 20)
val results = response.stream().toList()
assertEquals(20, results.size)
results.forEach {
val method = it as APIMethod
assertTrue(method.qualifiedName.startsWith(connectionQN))
assertFalse(method.name.isNullOrBlank())
// Every method should have a parent APIPath
assertNotNull(method.apiPath)
assertTrue(method.apiPath is APIPath)
}
// Verify a specific method: PUT /pet should have request and response
val putPet = results.first { (it as APIMethod).name == "PUT /pet" } as APIMethod
assertNotNull(putPet.apiMethodRequest, "PUT /pet should have a request body")
assertNotNull(putPet.apiMethodResponse, "PUT /pet should have a response body")
assertNotNull(putPet.apiMethodResponseCodes, "PUT /pet should have response codes")
assertTrue(putPet.apiMethodResponseCodes.containsKey("200"), "PUT /pet should have a 200 response")
}

@Test
fun methodRequestSchemaLinked() {
// Verify that methods with request bodies are linked to APIObjects
val connectionQN = Connection.findByName(client, testId, connectorType)?.get(0)?.qualifiedName!!
val request =
APIMethod
.select(client)
.where(APIMethod.QUALIFIED_NAME.startsWith(connectionQN))
.includeOnResults(APIMethod.NAME)
.includeOnResults(APIMethod.API_METHOD_REQUEST_SCHEMA)
.includeOnRelations(APIObject.QUALIFIED_NAME)
.toRequest()
val response = retrySearchUntil(request, 20)
val results = response.stream().toList()
// POST /pet should have its request schema linked to the Pet APIObject
val postPet = results.first { (it as APIMethod).name == "POST /pet" } as APIMethod
assertNotNull(postPet.apiMethodRequestSchema, "POST /pet should be linked to a request schema APIObject")
assertTrue(
postPet.apiMethodRequestSchema.uniqueAttributes.qualifiedName
.contains("Pet"),
"POST /pet request schema should reference the Pet object",
)
}

@Test
fun methodResponseSchemaLinked() {
// Verify that methods with $ref response schemas are linked to APIObjects
val connectionQN = Connection.findByName(client, testId, connectorType)?.get(0)?.qualifiedName!!
val request =
APIMethod
.select(client)
.where(APIMethod.QUALIFIED_NAME.startsWith(connectionQN))
.includeOnResults(APIMethod.NAME)
.includeOnResults(APIMethod.API_METHOD_RESPONSE_SCHEMAS)
.includeOnResults(APIMethod.API_METHOD_RESPONSE_CODES)
.includeOnRelations(APIObject.QUALIFIED_NAME)
.toRequest()
val response = retrySearchUntil(request, 20)
val results = response.stream().toList()
// GET /pet/{petId} should have a response schema linked to the Pet APIObject
val getPetById = results.first { (it as APIMethod).name == "GET /pet/{petId}" } as APIMethod
assertNotNull(getPetById.apiMethodResponseSchemas, "GET /pet/{petId} should have response schemas")
assertFalse(getPetById.apiMethodResponseSchemas.isEmpty(), "GET /pet/{petId} should have at least one response schema")
assertTrue(
getPetById.apiMethodResponseSchemas.any {
(it as APIObject).uniqueAttributes.qualifiedName.contains("Pet")
},
"GET /pet/{petId} response should reference the Pet object",
)
// Verify response codes map is populated
assertNotNull(getPetById.apiMethodResponseCodes, "GET /pet/{petId} should have response codes")
assertTrue(getPetById.apiMethodResponseCodes.containsKey("200"), "GET /pet/{petId} should have a 200 response code")
}

@Test
fun objectRefFieldsLinked() {
// Verify that APIField objects referencing other schemas have apiIsObjectReference and apiObjectQualifiedName set
val connectionQN = Connection.findByName(client, testId, connectorType)?.get(0)?.qualifiedName!!
val request =
APIField
.select(client)
.where(APIField.QUALIFIED_NAME.startsWith(connectionQN))
.where(APIField.API_IS_OBJECT_REFERENCE.eq(true))
.includeOnResults(APIField.NAME)
.includeOnResults(APIField.API_IS_OBJECT_REFERENCE)
.includeOnResults(APIField.API_OBJECT_QUALIFIED_NAME)
.toRequest()
val response = retrySearchUntil(request, 1)
val results = response.stream().toList()
assertTrue(results.isNotEmpty(), "Should have fields with object references")
// Pet.category should reference the Category schema
val categoryField = results.firstOrNull { (it as APIField).name == "category" }
if (categoryField != null) {
val field = categoryField as APIField
assertEquals(true, field.apiIsObjectReference)
assertNotNull(field.apiObjectQualifiedName, "category field should have apiObjectQualifiedName")
assertTrue(field.apiObjectQualifiedName.contains("Category"), "category field should reference the Category schema")
}
}

@Test
fun filesCreated() {
validateFilesExist(files)
Expand Down
12 changes: 12 additions & 0 deletions sdk/src/main/java/com/atlan/model/assets/APIField.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ public class APIField extends Asset implements IAPIField, IAPI, ICatalog, IAsset
@Builder.Default
String typeName = TYPE_NAME;

/** Type of API body this field belongs to (e.g. "request", "response/200"). */
@Attribute
String apiBodyType;

/** External documentation of the API. */
@Attribute
@Singular
Expand All @@ -70,6 +74,10 @@ public class APIField extends Asset implements IAPIField, IAPI, ICatalog, IAsset
@Attribute
Boolean apiIsObjectReference;

/** Qualified name of the API method this field belongs to. */
@Attribute
String apiMethodQualifiedName;

/** APIObject asset containing this APIField. */
@Attribute
IAPIObject apiObject;
Expand All @@ -78,6 +86,10 @@ public class APIField extends Asset implements IAPIField, IAPI, ICatalog, IAsset
@Attribute
String apiObjectQualifiedName;

/** Qualified name of the API path this field belongs to. */
@Attribute
String apiPathQualifiedName;

/** APIQuery asset containing this APIField. */
@Attribute
IAPIQuery apiQuery;
Expand Down
Loading