diff --git a/samples/packages/openapi-spec-loader/src/main/kotlin/OpenAPISpecLoader.kt b/samples/packages/openapi-spec-loader/src/main/kotlin/OpenAPISpecLoader.kt index 9b85b19c74..936bab334c 100644 --- a/samples/packages/openapi-spec-loader/src/main/kotlin/OpenAPISpecLoader.kt +++ b/samples/packages/openapi-spec-loader/src/main/kotlin/OpenAPISpecLoader.kt @@ -2,6 +2,9 @@ Copyright 2023 Atlan Pte. Ltd. */ import com.atlan.AtlanClient import com.atlan.exception.AtlanException +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.core.AssetMutationResponse @@ -10,15 +13,19 @@ import com.atlan.model.enums.CustomMetadataHandling import com.atlan.pkg.PackageContext import com.atlan.pkg.Utils import com.atlan.util.AssetBatch +import com.fasterxml.jackson.databind.ObjectMapper import io.swagger.v3.oas.models.OpenAPI import io.swagger.v3.oas.models.Operation +import io.swagger.v3.oas.models.media.Schema import io.swagger.v3.parser.OpenAPIV3Parser import java.nio.file.Paths +import java.util.concurrent.ThreadLocalRandom import java.util.concurrent.atomic.AtomicLong import kotlin.system.exitProcess object OpenAPISpecLoader { private val logger = Utils.getLogger(OpenAPISpecLoader.javaClass.name) + private val jsonMapper = ObjectMapper() /** * Actually run the loader, taking all settings from environment variables. @@ -151,6 +158,7 @@ object OpenAPISpecLoader { spec: OpenAPISpecReader, batchSize: Int, ) { + // --- Step 1: Save the APISpec (unchanged from original) --- val toCreate = APISpec .creator(spec.title, connectionQN) @@ -186,46 +194,412 @@ object OpenAPISpecLoader { logger.error("Unable to save the APISpec.", e) exitProcess(5) } + + // --- Step 2: Create APIObjects and APIFields from components/schemas --- + val objectQNs = mutableMapOf() // schemaName -> qualifiedName + val schemas = spec.schemas + if (!schemas.isNullOrEmpty()) { + logger.info { "Creating APIObjects for ${schemas.size} component schema(s)" } + AssetBatch(client, batchSize, AtlanTagHandling.IGNORE, CustomMetadataHandling.MERGE, true).use { objectBatch -> + AssetBatch(client, batchSize, AtlanTagHandling.IGNORE, CustomMetadataHandling.MERGE, true).use { fieldBatch -> + try { + for ((schemaName, schema) in schemas) { + val objectQN = "$specQN/schemas/$schemaName" + objectQNs[schemaName] = objectQN + val properties = schema.properties ?: emptyMap() + val apiObject = + APIObject + ._internal() + .guid("-" + ThreadLocalRandom.current().nextLong(0, Long.MAX_VALUE - 1)) + .qualifiedName(objectQN) + .name(schemaName) + .connectionQualifiedName(connectionQN) + .apiSpecQualifiedName(specQN) + .apiSpecName(spec.title) + .apiSpecType(spec.openAPIVersion) + .apiFieldCount(properties.size.toLong()) + .build() + objectBatch.add(apiObject) + } + objectBatch.flush() + + // Second pass: create APIFields (after all APIObjects exist) + for ((schemaName, schema) in schemas) { + val objectQN = objectQNs[schemaName]!! + val properties = schema.properties ?: emptyMap() + for ((fieldName, fieldSchema) in properties) { + val fieldQN = "$objectQN/$fieldName" + val refSchemaName = extractRefSchemaName(fieldSchema) + val isObjectRef = refSchemaName != null + val fieldType = resolveFieldType(fieldSchema) + val fieldTypeSecondary = resolveFieldTypeSecondary(fieldSchema) + val fieldBuilder = + APIField + ._internal() + .guid("-" + ThreadLocalRandom.current().nextLong(0, Long.MAX_VALUE - 1)) + .qualifiedName(fieldQN) + .name(fieldName) + .connectionQualifiedName(connectionQN) + .apiSpecQualifiedName(specQN) + .apiSpecName(spec.title) + .apiSpecType(spec.openAPIVersion) + .apiFieldType(fieldType) + .apiObject(APIObject.refByQualifiedName(objectQN)) + if (fieldTypeSecondary != null) { + fieldBuilder.apiFieldTypeSecondary(fieldTypeSecondary) + } + if (isObjectRef) { + fieldBuilder.apiIsObjectReference(true) + val refQN = objectQNs[refSchemaName] + if (refQN != null) { + fieldBuilder.apiObjectQualifiedName(refQN) + } + } + fieldBatch.add(fieldBuilder.build()) + } + } + fieldBatch.flush() + } catch (e: AtlanException) { + logger.error("Unable to bulk-save API objects/fields.", e) + } + } + } + logger.info { "Created ${objectQNs.size} APIObject(s) with their APIField children" } + } + + // --- Step 3: Create APIPaths (unchanged) and APIMethod per operation --- val totalCount = spec.paths?.size!!.toLong() if (totalCount > 0) { logger.info { "Creating an APIPath for each path defined within the spec (total: $totalCount)" } - AssetBatch(client, batchSize, AtlanTagHandling.IGNORE, CustomMetadataHandling.MERGE, true).use { batch -> - try { - val assetCount = AtomicLong(0) - for (apiPath in spec.paths.entries) { - val pathUrl = apiPath.key - val pathDetails = apiPath.value - val operations = mutableListOf() - val desc = StringBuilder() - desc.append("| Method | Summary|\n|---|---|\n") - addOperationDetails(pathDetails.get, "GET", operations, desc) - addOperationDetails(pathDetails.post, "POST", operations, desc) - addOperationDetails(pathDetails.put, "PUT", operations, desc) - addOperationDetails(pathDetails.patch, "PATCH", operations, desc) - addOperationDetails(pathDetails.delete, "DELETE", operations, desc) - val path = - APIPath - .creator(pathUrl, specQN) - .description(desc.toString()) - .apiPathRawURI(pathUrl) - .apiPathSummary(pathDetails.summary) - .apiPathAvailableOperations(operations) - .apiPathIsTemplated(pathUrl.contains("{") && pathUrl.contains("}")) - .build() - batch.add(path) - Utils.logProgress(assetCount, totalCount, logger, batchSize) + AssetBatch(client, batchSize, AtlanTagHandling.IGNORE, CustomMetadataHandling.MERGE, true).use { pathBatch -> + AssetBatch(client, batchSize, AtlanTagHandling.IGNORE, CustomMetadataHandling.MERGE, true).use { methodBatch -> + AssetBatch(client, batchSize, AtlanTagHandling.IGNORE, CustomMetadataHandling.MERGE, true).use { inlineObjectBatch -> + AssetBatch(client, batchSize, AtlanTagHandling.IGNORE, CustomMetadataHandling.MERGE, true).use { inlineFieldBatch -> + try { + val assetCount = AtomicLong(0) + for (apiPath in spec.paths.entries) { + val pathUrl = apiPath.key + val pathDetails = apiPath.value + + // --- APIPath creation (unchanged from original) --- + val operations = mutableListOf() + val desc = StringBuilder() + desc.append("| Method | Summary|\n|---|---|\n") + addOperationDetails(pathDetails.get, "GET", operations, desc) + addOperationDetails(pathDetails.post, "POST", operations, desc) + addOperationDetails(pathDetails.put, "PUT", operations, desc) + addOperationDetails(pathDetails.patch, "PATCH", operations, desc) + addOperationDetails(pathDetails.delete, "DELETE", operations, desc) + val path = + APIPath + .creator(pathUrl, specQN) + .description(desc.toString()) + .apiPathRawURI(pathUrl) + .apiPathSummary(pathDetails.summary) + .apiPathAvailableOperations(operations) + .apiPathIsTemplated(pathUrl.contains("{") && pathUrl.contains("}")) + .build() + pathBatch.add(path) + val pathQN = path.qualifiedName + + // --- APIMethod creation per operation (with inline schema support) --- + val methods = listOf("GET" to pathDetails.get, "POST" to pathDetails.post, "PUT" to pathDetails.put, "PATCH" to pathDetails.patch, "DELETE" to pathDetails.delete) + for ((httpMethod, op) in methods) { + createMethodIfPresent(op, httpMethod, pathQN, pathUrl, specQN, connectionQN, spec, objectQNs, methodBatch, inlineObjectBatch, inlineFieldBatch) + } + + Utils.logProgress(assetCount, totalCount, logger, batchSize) + } + pathBatch.flush() + inlineObjectBatch.flush() + inlineFieldBatch.flush() + methodBatch.flush() + Utils.logProgress(assetCount, totalCount, logger, batchSize) + } catch (e: AtlanException) { + logger.error("Unable to bulk-save API paths/methods.", e) + } + } } - batch.flush() - Utils.logProgress(assetCount, totalCount, logger, batchSize) - } catch (e: AtlanException) { - logger.error("Unable to bulk-save API paths.", e) } } } } + /** + * Create an APIMethod asset for a single HTTP operation on a path, if the operation exists. + * + * @param operation the Swagger operation (may be null if this HTTP method is not defined on the path) + * @param httpMethod the HTTP method name (GET, POST, PUT, PATCH, DELETE) + * @param pathQN qualified name of the parent APIPath + * @param pathUrl the raw path URL (e.g., "/pet/{petId}") + * @param specQN qualified name of the parent APISpec + * @param connectionQN qualified name of the connection + * @param spec the OpenAPISpecReader for spec-level metadata + * @param objectQNs map of schema name to APIObject qualified name + * @param batch the AssetBatch to add the method to + */ + private fun createMethodIfPresent( + operation: Operation?, + httpMethod: String, + pathQN: String, + pathUrl: String, + specQN: String, + connectionQN: String, + spec: OpenAPISpecReader, + objectQNs: MutableMap, + methodBatch: AssetBatch, + objectBatch: AssetBatch, + fieldBatch: AssetBatch, + ) { + if (operation == null) return + + val methodName = "$httpMethod $pathUrl" + val methodQN = "$pathQN/$httpMethod" + + val methodBuilder = + APIMethod + ._internal() + .guid("-" + ThreadLocalRandom.current().nextLong(0, Long.MAX_VALUE - 1)) + .qualifiedName(methodQN) + .name(methodName) + .connectionQualifiedName(connectionQN) + .apiSpecQualifiedName(specQN) + .apiSpecName(spec.title) + .apiSpecType(spec.openAPIVersion) + .description(operation.summary ?: operation.description ?: "") + .apiPath(APIPath.refByQualifiedName(pathQN)) + + // Request body blob + schema relationship + val requestSchema = extractRequestSchema(operation) + if (requestSchema != null) { + methodBuilder.apiMethodRequest(serializeSchema(requestSchema)) + val requestRefName = extractRefSchemaName(requestSchema) + if (requestRefName != null) { + val requestObjectQN = objectQNs[requestRefName] + if (requestObjectQN != null) { + methodBuilder.apiMethodRequestSchema(APIObject.refByQualifiedName(requestObjectQN)) + } + } else if (requestSchema.properties != null && requestSchema.properties.isNotEmpty()) { + // Inline request schema with properties — create synthetic APIObject + APIFields + val syntheticName = "${httpMethod}_${sanitizePath(pathUrl)}_Request" + val syntheticQN = + createInlineSchemaObject( + syntheticName, + requestSchema, + specQN, + connectionQN, + spec, + objectQNs, + objectBatch, + fieldBatch, + ) + methodBuilder.apiMethodRequestSchema(APIObject.refByQualifiedName(syntheticQN)) + } + } + + // Response bodies: blob for primary response + schema relationships for all + val responseCodes = mutableMapOf() + val responseSchemas = mutableListOf() // QNs for relationship set + var primaryResponseBlob: String? = null + if (operation.responses != null) { + for ((statusCode, apiResponse) in operation.responses) { + val responseSchema = extractResponseSchema(apiResponse) + if (responseSchema != null) { + // Use the first success response (2xx) as the primary blob + if (primaryResponseBlob == null && statusCode.startsWith("2")) { + primaryResponseBlob = serializeSchema(responseSchema) + } + val refName = extractRefSchemaName(responseSchema) + if (refName != null) { + val responseObjectQN = objectQNs[refName] + if (responseObjectQN != null) { + responseCodes[statusCode] = responseObjectQN + responseSchemas.add(responseObjectQN) + } + } else if (responseSchema.properties != null && responseSchema.properties.isNotEmpty()) { + // Inline response schema with properties — create synthetic APIObject + APIFields + val syntheticName = "${httpMethod}_${sanitizePath(pathUrl)}_${statusCode}_Response" + val syntheticQN = + createInlineSchemaObject( + syntheticName, + responseSchema, + specQN, + connectionQN, + spec, + objectQNs, + objectBatch, + fieldBatch, + ) + responseCodes[statusCode] = syntheticQN + responseSchemas.add(syntheticQN) + } else { + // Inline primitive/array schema — record in response codes map only + val inlineName = "${httpMethod}_${sanitizePath(pathUrl)}_$statusCode" + responseCodes[statusCode] = inlineName + } + } + } + } + if (primaryResponseBlob != null) { + methodBuilder.apiMethodResponse(primaryResponseBlob) + } + if (responseCodes.isNotEmpty()) { + methodBuilder.apiMethodResponseCodes(responseCodes) + } + for (responseQN in responseSchemas) { + methodBuilder.apiMethodResponseSchema(APIObject.refByQualifiedName(responseQN)) + } + + methodBatch.add(methodBuilder.build()) + } + + /** + * Create a synthetic APIObject (and its APIFields) for an inline schema that has properties. + * Returns the qualified name of the created APIObject. + */ + private fun createInlineSchemaObject( + syntheticName: String, + schema: Schema<*>, + specQN: String, + connectionQN: String, + spec: OpenAPISpecReader, + objectQNs: MutableMap, + objectBatch: AssetBatch, + fieldBatch: AssetBatch, + ): String { + val objectQN = "$specQN/schemas/$syntheticName" + objectQNs[syntheticName] = objectQN + val properties = schema.properties ?: emptyMap() + val apiObject = + APIObject + ._internal() + .guid("-" + ThreadLocalRandom.current().nextLong(0, Long.MAX_VALUE - 1)) + .qualifiedName(objectQN) + .name(syntheticName) + .connectionQualifiedName(connectionQN) + .apiSpecQualifiedName(specQN) + .apiSpecName(spec.title) + .apiSpecType(spec.openAPIVersion) + .apiFieldCount(properties.size.toLong()) + .build() + objectBatch.add(apiObject) + for ((fieldName, fieldSchema) in properties) { + val fieldQN = "$objectQN/$fieldName" + val refSchemaName = extractRefSchemaName(fieldSchema) + val isObjectRef = refSchemaName != null + val fieldType = resolveFieldType(fieldSchema) + val fieldTypeSecondary = resolveFieldTypeSecondary(fieldSchema) + val fieldBuilder = + APIField + ._internal() + .guid("-" + ThreadLocalRandom.current().nextLong(0, Long.MAX_VALUE - 1)) + .qualifiedName(fieldQN) + .name(fieldName) + .connectionQualifiedName(connectionQN) + .apiSpecQualifiedName(specQN) + .apiSpecName(spec.title) + .apiSpecType(spec.openAPIVersion) + .apiFieldType(fieldType) + .apiObject(APIObject.refByQualifiedName(objectQN)) + if (fieldTypeSecondary != null) { + fieldBuilder.apiFieldTypeSecondary(fieldTypeSecondary) + } + if (isObjectRef) { + fieldBuilder.apiIsObjectReference(true) + val refQN = objectQNs[refSchemaName] + if (refQN != null) { + fieldBuilder.apiObjectQualifiedName(refQN) + } + } + fieldBatch.add(fieldBuilder.build()) + } + return objectQN + } + + /** + * Sanitize a path URL for use in synthetic schema names. + */ + private fun sanitizePath(pathUrl: String): String = + pathUrl + .replace("/", "_") + .replace("{", "") + .replace("}", "") + .trimStart('_') + + /** + * Extract the request body schema from an operation, preferring application/json. + */ + private fun extractRequestSchema(operation: Operation): Schema<*>? { + val content = operation.requestBody?.content ?: return null + return content["application/json"]?.schema + ?: content["application/xml"]?.schema + ?: content.values.firstOrNull()?.schema + } + + /** + * Extract the response body schema from an API response, preferring application/json. + */ + private fun extractResponseSchema(response: io.swagger.v3.oas.models.responses.ApiResponse): Schema<*>? { + val content = response.content ?: return null + return content["application/json"]?.schema + ?: content["application/xml"]?.schema + ?: content.values.firstOrNull()?.schema + } + + /** + * Extract the schema name from a $ref string like "#/components/schemas/Pet". + * Returns null if the schema is inline (no $ref). + */ + private fun extractRefSchemaName(schema: Schema<*>): String? { + val ref = schema.`$ref` + if (ref != null && ref.startsWith("#/components/schemas/")) { + return ref.removePrefix("#/components/schemas/") + } + // Check if this is an array of $ref items + if (schema.type == "array" && schema.items?.`$ref` != null) { + val itemRef = schema.items.`$ref` + if (itemRef.startsWith("#/components/schemas/")) { + return itemRef.removePrefix("#/components/schemas/") + } + } + return null + } + + /** + * Resolve the primary type of a field from its schema. + */ + private fun resolveFieldType(schema: Schema<*>): String { + // If it's a $ref, resolve to "object" + if (schema.`$ref` != null) return "object" + val type = schema.type ?: "object" + val format = schema.format + return if (format != null) "$type/$format" else type + } + + /** + * Resolve the secondary type of a field (e.g., for array types, the secondary is "array"). + */ + private fun resolveFieldTypeSecondary(schema: Schema<*>): String? { + if (schema.type == "array") { + val itemType = schema.items?.type ?: if (schema.items?.`$ref` != null) "object" else "string" + return "array" + } + return null + } + + /** + * Serialize a schema to a JSON string for the blob attributes. + */ + private fun serializeSchema(schema: Schema<*>): String = + try { + jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(schema) + } catch (e: Exception) { + schema.toString() + } + /** * Add the details of the provided operation to the details captured for the APIPath. + * (Unchanged from original — preserved for backward compatibility.) * * @param operation the operation to include (if non-null) as one that exists for the path * @param name the name of the operation @@ -261,6 +635,7 @@ object OpenAPISpecLoader { val sourceURL: String val openAPIVersion: String val paths: io.swagger.v3.oas.models.Paths? + val schemas: Map>? val title: String val description: String val termsOfServiceURL: String @@ -278,6 +653,7 @@ object OpenAPISpecLoader { sourceURL = url openAPIVersion = spec.openapi paths = spec.paths + schemas = spec.components?.schemas title = spec.info?.title ?: "" description = spec.info?.description ?: "" termsOfServiceURL = spec.info?.termsOfService ?: "" diff --git a/samples/packages/openapi-spec-loader/src/test/kotlin/ImportJsonTest.kt b/samples/packages/openapi-spec-loader/src/test/kotlin/ImportJsonTest.kt index 9178ace4f6..3d248e5236 100644 --- a/samples/packages/openapi-spec-loader/src/test/kotlin/ImportJsonTest.kt +++ b/samples/packages/openapi-spec-loader/src/test/kotlin/ImportJsonTest.kt @@ -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 @@ -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) diff --git a/sdk/src/main/java/com/atlan/model/assets/APIMethod.java b/sdk/src/main/java/com/atlan/model/assets/APIMethod.java new file mode 100644 index 0000000000..04b995529a --- /dev/null +++ b/sdk/src/main/java/com/atlan/model/assets/APIMethod.java @@ -0,0 +1,789 @@ +/* SPDX-License-Identifier: Apache-2.0 + Copyright 2022 Atlan Pte. Ltd. */ +package com.atlan.model.assets; + +import com.atlan.AtlanClient; +import com.atlan.exception.AtlanException; +import com.atlan.exception.ErrorCode; +import com.atlan.exception.InvalidRequestException; +import com.atlan.exception.NotFoundException; +import com.atlan.model.enums.AtlanAnnouncementType; +import com.atlan.model.enums.CertificateStatus; +import com.atlan.model.fields.AtlanField; +import com.atlan.model.relations.Reference; +import com.atlan.model.relations.UniqueAttributes; +import com.atlan.model.search.FluentSearch; +import com.atlan.util.StringUtils; +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.SortedSet; +import java.util.concurrent.ThreadLocalRandom; +import javax.annotation.processing.Generated; +import lombok.*; +import lombok.experimental.SuperBuilder; +import lombok.extern.slf4j.Slf4j; + +/** + * Instance of an API method (operation) on a path in Atlan. + * Represents a single HTTP method such as GET, POST, PUT, DELETE on an APIPath. + */ +@Generated(value = "com.atlan.generators.ModelGeneratorV2") +@Getter +@SuperBuilder(toBuilder = true, builderMethodName = "_internal") +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Slf4j +@SuppressWarnings({"cast", "serial"}) +public class APIMethod extends Asset implements IAPIMethod, IAPI, ICatalog, IAsset, IReferenceable { + private static final long serialVersionUID = 2L; + + public static final String TYPE_NAME = "APIMethod"; + + // Override all default methods that conflict between IAPI and ICatalog interfaces. + @Override + public IApplication getApplication() { + return null; + } + + @Override + public IApplicationField getApplicationField() { + return null; + } + + @Override + public IDataContract getDataContractLatest() { + return null; + } + + @Override + public IDataContract getDataContractLatestCertified() { + return null; + } + + @Override + public IReadme getReadme() { + return null; + } + + @Override + public SortedSet getInputToAirflowTasks() { + return null; + } + + @Override + public SortedSet getOutputFromAirflowTasks() { + return null; + } + + @Override + public SortedSet getAnomaloChecks() { + return null; + } + + @Override + public SortedSet getUserDefRelationshipFroms() { + return null; + } + + @Override + public SortedSet getUserDefRelationshipTos() { + return null; + } + + @Override + public SortedSet getInputPortDataProducts() { + return null; + } + + @Override + public SortedSet getOutputPortDataProducts() { + return null; + } + + @Override + public SortedSet getDqBaseDatasetRules() { + return null; + } + + @Override + public SortedSet getDqReferenceDatasetRules() { + return null; + } + + @Override + public SortedSet getFiles() { + return null; + } + + @Override + public SortedSet getAssignedTerms() { + return null; + } + + @Override + public SortedSet getInputToProcesses() { + return null; + } + + @Override + public SortedSet getOutputFromProcesses() { + return null; + } + + @Override + public SortedSet getLinks() { + return null; + } + + @Override + public SortedSet getMcIncidents() { + return null; + } + + @Override + public SortedSet getMcMonitors() { + return null; + } + + @Override + public SortedSet getMetrics() { + return null; + } + + @Override + public SortedSet getModelImplementedAttributes() { + return null; + } + + @Override + public SortedSet getModelImplementedEntities() { + return null; + } + + @Override + public SortedSet getPartialChildFields() { + return null; + } + + @Override + public SortedSet getPartialChildObjects() { + return null; + } + + @Override + public SortedSet getSchemaRegistrySubjects() { + return null; + } + + @Override + public SortedSet getSodaChecks() { + return null; + } + + @Override + public SortedSet getInputToSparkJobs() { + return null; + } + + @Override + public SortedSet getOutputFromSparkJobs() { + return null; + } + + /** Fixed typeName for APIMethods. */ + @Getter(onMethod_ = {@Override}) + @Builder.Default + String typeName = TYPE_NAME; + + /** External documentation of the API. */ + @Attribute + @Singular + Map apiExternalDocs; + + /** Whether authentication is optional (true) or required (false). */ + @Attribute + Boolean apiIsAuthOptional; + + /** If this asset refers to an APIObject */ + @Attribute + Boolean apiIsObjectReference; + + /** Request body or schema information for this API method. */ + @Attribute + String apiMethodRequest; + + /** APIObject schema describing this method's request body. */ + @Attribute + IAPIObject apiMethodRequestSchema; + + /** Response body or schema information for this API method. */ + @Attribute + String apiMethodResponse; + + /** Map of HTTP response status codes to the qualified names of the APIObject schemas that describe each response. */ + @Attribute + @Singular + Map apiMethodResponseCodes; + + /** APIObject schemas describing this method's response bodies. */ + @Attribute + @Singular("apiMethodResponseSchema") + SortedSet apiMethodResponseSchemas; + + /** Qualified name of the APIObject that is referred to by this asset. When apiIsObjectReference is true. */ + @Attribute + String apiObjectQualifiedName; + + /** API path on which this method operates. */ + @Attribute + IAPIPath apiPath; + + /** Simple name of the API spec, if this asset is contained in an API spec. */ + @Attribute + String apiSpecName; + + /** Unique name of the API spec, if this asset is contained in an API spec. */ + @Attribute + String apiSpecQualifiedName; + + /** Type of API, for example: OpenAPI, GraphQL, etc. */ + @Attribute + String apiSpecType; + + /** Version of the API specification. */ + @Attribute + String apiSpecVersion; + + /** Tasks to which this asset provides input. */ + @Attribute + @Singular + SortedSet inputToAirflowTasks; + + /** Processes to which this asset provides input. */ + @Attribute + @Singular + SortedSet inputToProcesses; + + /** TBC */ + @Attribute + @Singular + SortedSet inputToSparkJobs; + + /** Attributes implemented by this asset. */ + @Attribute + @Singular + SortedSet modelImplementedAttributes; + + /** Entities implemented by this asset. */ + @Attribute + @Singular + SortedSet modelImplementedEntities; + + /** Tasks from which this asset is output. */ + @Attribute + @Singular + SortedSet outputFromAirflowTasks; + + /** Processes from which this asset is produced as output. */ + @Attribute + @Singular + SortedSet outputFromProcesses; + + /** TBC */ + @Attribute + @Singular + SortedSet outputFromSparkJobs; + + /** + * Builds the minimal object necessary to create a relationship to a APIMethod, from a potentially + * more-complete APIMethod object. + * + * @return the minimal object necessary to relate to the APIMethod + * @throws InvalidRequestException if any of the minimal set of required properties for a APIMethod relationship are not found in the initial object + */ + @Override + public APIMethod trimToReference() throws InvalidRequestException { + if (this.getGuid() != null && !this.getGuid().isEmpty()) { + return refByGuid(this.getGuid()); + } + if (this.getQualifiedName() != null && !this.getQualifiedName().isEmpty()) { + return refByQualifiedName(this.getQualifiedName()); + } + if (this.getUniqueAttributes() != null + && this.getUniqueAttributes().getQualifiedName() != null + && !this.getUniqueAttributes().getQualifiedName().isEmpty()) { + return refByQualifiedName(this.getUniqueAttributes().getQualifiedName()); + } + throw new InvalidRequestException( + ErrorCode.MISSING_REQUIRED_RELATIONSHIP_PARAM, TYPE_NAME, "guid, qualifiedName"); + } + + /** + * Start a fluent search that will return all APIMethod assets. + * Additional conditions can be chained onto the returned search before any + * asset retrieval is attempted, ensuring all conditions are pushed-down for + * optimal retrieval. Only active (non-archived) APIMethod assets will be included. + * + * @param client connectivity to the Atlan tenant from which to retrieve the assets + * @return a fluent search that includes all APIMethod assets + */ + public static FluentSearch.FluentSearchBuilder select(AtlanClient client) { + return select(client, false); + } + + /** + * Start a fluent search that will return all APIMethod assets. + * Additional conditions can be chained onto the returned search before any + * asset retrieval is attempted, ensuring all conditions are pushed-down for + * optimal retrieval. + * + * @param client connectivity to the Atlan tenant from which to retrieve the assets + * @param includeArchived when true, archived (soft-deleted) APIMethods will be included + * @return a fluent search that includes all APIMethod assets + */ + public static FluentSearch.FluentSearchBuilder select(AtlanClient client, boolean includeArchived) { + FluentSearch.FluentSearchBuilder builder = + FluentSearch.builder(client).where(Asset.TYPE_NAME.eq(TYPE_NAME)); + if (!includeArchived) { + builder.active(); + } + return builder; + } + + /** + * Reference to a APIMethod by GUID. Use this to create a relationship to this APIMethod, + * where the relationship should be replaced. + * + * @param guid the GUID of the APIMethod to reference + * @return reference to a APIMethod that can be used for defining a relationship to a APIMethod + */ + public static APIMethod refByGuid(String guid) { + return refByGuid(guid, Reference.SaveSemantic.REPLACE); + } + + /** + * Reference to a APIMethod by GUID. Use this to create a relationship to this APIMethod, + * where you want to further control how that relationship should be updated (i.e. replaced, + * appended, or removed). + * + * @param guid the GUID of the APIMethod to reference + * @param semantic how to save this relationship (replace all with this, append it, or remove it) + * @return reference to a APIMethod that can be used for defining a relationship to a APIMethod + */ + public static APIMethod refByGuid(String guid, Reference.SaveSemantic semantic) { + return APIMethod._internal().guid(guid).semantic(semantic).build(); + } + + /** + * Reference to a APIMethod by qualifiedName. Use this to create a relationship to this APIMethod, + * where the relationship should be replaced. + * + * @param qualifiedName the qualifiedName of the APIMethod to reference + * @return reference to a APIMethod that can be used for defining a relationship to a APIMethod + */ + public static APIMethod refByQualifiedName(String qualifiedName) { + return refByQualifiedName(qualifiedName, Reference.SaveSemantic.REPLACE); + } + + /** + * Reference to a APIMethod by qualifiedName. Use this to create a relationship to this APIMethod, + * where you want to further control how that relationship should be updated (i.e. replaced, + * appended, or removed). + * + * @param qualifiedName the qualifiedName of the APIMethod to reference + * @param semantic how to save this relationship (replace all with this, append it, or remove it) + * @return reference to a APIMethod that can be used for defining a relationship to a APIMethod + */ + public static APIMethod refByQualifiedName(String qualifiedName, Reference.SaveSemantic semantic) { + return APIMethod._internal() + .uniqueAttributes( + UniqueAttributes.builder().qualifiedName(qualifiedName).build()) + .semantic(semantic) + .build(); + } + + /** + * Retrieves a APIMethod by one of its identifiers, complete with all of its relationships. + * + * @param client connectivity to the Atlan tenant from which to retrieve the asset + * @param id of the APIMethod to retrieve, either its GUID or its full qualifiedName + * @return the requested full APIMethod, complete with all of its relationships + * @throws AtlanException on any error during the API invocation, such as the {@link NotFoundException} if the APIMethod does not exist or the provided GUID is not a APIMethod + */ + @JsonIgnore + public static APIMethod get(AtlanClient client, String id) throws AtlanException { + return get(client, id, false); + } + + /** + * Retrieves a APIMethod by one of its identifiers, optionally complete with all of its relationships. + * + * @param client connectivity to the Atlan tenant from which to retrieve the asset + * @param id of the APIMethod to retrieve, either its GUID or its full qualifiedName + * @param includeAllRelationships if true, all the asset's relationships will also be retrieved; if false, no relationships will be retrieved + * @return the requested full APIMethod, optionally complete with all of its relationships + * @throws AtlanException on any error during the API invocation, such as the {@link NotFoundException} if the APIMethod does not exist or the provided GUID is not a APIMethod + */ + @JsonIgnore + public static APIMethod get(AtlanClient client, String id, boolean includeAllRelationships) throws AtlanException { + if (id == null) { + throw new NotFoundException(ErrorCode.ASSET_NOT_FOUND_BY_GUID, "(null)"); + } else if (StringUtils.isUUID(id)) { + Asset asset = Asset.get(client, id, includeAllRelationships); + if (asset == null) { + throw new NotFoundException(ErrorCode.ASSET_NOT_FOUND_BY_GUID, id); + } else if (asset instanceof APIMethod) { + return (APIMethod) asset; + } else { + throw new NotFoundException(ErrorCode.ASSET_NOT_TYPE_REQUESTED, id, TYPE_NAME); + } + } else { + Asset asset = Asset.get(client, TYPE_NAME, id, includeAllRelationships); + if (asset instanceof APIMethod) { + return (APIMethod) asset; + } else { + throw new NotFoundException(ErrorCode.ASSET_NOT_FOUND_BY_QN, id, TYPE_NAME); + } + } + } + + /** + * Retrieves a APIMethod by one of its identifiers, with only the requested attributes (and relationships). + * + * @param client connectivity to the Atlan tenant from which to retrieve the asset + * @param id of the APIMethod to retrieve, either its GUID or its full qualifiedName + * @param attributes to retrieve for the APIMethod, including any relationships + * @return the requested APIMethod, with only its minimal information and the requested attributes (and relationships) + * @throws AtlanException on any error during the API invocation, such as the {@link NotFoundException} if the APIMethod does not exist or the provided GUID is not a APIMethod + */ + @JsonIgnore + public static APIMethod get(AtlanClient client, String id, Collection attributes) + throws AtlanException { + return get(client, id, attributes, Collections.emptyList()); + } + + /** + * Retrieves a APIMethod by one of its identifiers, with only the requested attributes (and relationships). + * + * @param client connectivity to the Atlan tenant from which to retrieve the asset + * @param id of the APIMethod to retrieve, either its GUID or its full qualifiedName + * @param attributes to retrieve for the APIMethod, including any relationships + * @param attributesOnRelated to retrieve on each relationship retrieved for the APIMethod + * @return the requested APIMethod, with only its minimal information and the requested attributes (and relationships) + * @throws AtlanException on any error during the API invocation, such as the {@link NotFoundException} if the APIMethod does not exist or the provided GUID is not a APIMethod + */ + @JsonIgnore + public static APIMethod get( + AtlanClient client, + String id, + Collection attributes, + Collection attributesOnRelated) + throws AtlanException { + if (id == null) { + throw new NotFoundException(ErrorCode.ASSET_NOT_FOUND_BY_GUID, "(null)"); + } else if (StringUtils.isUUID(id)) { + Optional asset = APIMethod.select(client) + .where(APIMethod.GUID.eq(id)) + .includesOnResults(attributes) + .includesOnRelations(attributesOnRelated) + .includeRelationshipAttributes(true) + .pageSize(1) + .stream() + .findFirst(); + if (!asset.isPresent()) { + throw new NotFoundException(ErrorCode.ASSET_NOT_FOUND_BY_GUID, id); + } else if (asset.get() instanceof APIMethod) { + return (APIMethod) asset.get(); + } else { + throw new NotFoundException(ErrorCode.ASSET_NOT_TYPE_REQUESTED, id, TYPE_NAME); + } + } else { + Optional asset = APIMethod.select(client) + .where(APIMethod.QUALIFIED_NAME.eq(id)) + .includesOnResults(attributes) + .includesOnRelations(attributesOnRelated) + .includeRelationshipAttributes(true) + .pageSize(1) + .stream() + .findFirst(); + if (!asset.isPresent()) { + throw new NotFoundException(ErrorCode.ASSET_NOT_FOUND_BY_QN, id, TYPE_NAME); + } else if (asset.get() instanceof APIMethod) { + return (APIMethod) asset.get(); + } else { + throw new NotFoundException(ErrorCode.ASSET_NOT_TYPE_REQUESTED, id, TYPE_NAME); + } + } + } + + /** + * Restore the archived (soft-deleted) APIMethod to active. + * + * @param client connectivity to the Atlan tenant on which to restore the asset + * @param qualifiedName for the APIMethod + * @return true if the APIMethod is now active, and false otherwise + * @throws AtlanException on any API problems + */ + public static boolean restore(AtlanClient client, String qualifiedName) throws AtlanException { + return Asset.restore(client, TYPE_NAME, qualifiedName); + } + + /** + * Builds the minimal object necessary to update a APIMethod. + * + * @param qualifiedName of the APIMethod + * @param name of the APIMethod + * @return the minimal request necessary to update the APIMethod, as a builder + */ + public static APIMethodBuilder updater(String qualifiedName, String name) { + return APIMethod._internal() + .guid("-" + ThreadLocalRandom.current().nextLong(0, Long.MAX_VALUE - 1)) + .qualifiedName(qualifiedName) + .name(name); + } + + /** + * Builds the minimal object necessary to apply an update to a APIMethod, + * from a potentially more-complete APIMethod object. + * + * @return the minimal object necessary to update the APIMethod, as a builder + * @throws InvalidRequestException if any of the minimal set of required fields for a APIMethod are not present in the initial object + */ + @Override + public APIMethodBuilder trimToRequired() throws InvalidRequestException { + Map map = new HashMap<>(); + map.put("qualifiedName", this.getQualifiedName()); + map.put("name", this.getName()); + validateRequired(TYPE_NAME, map); + return updater(this.getQualifiedName(), this.getName()); + } + + public abstract static class APIMethodBuilder> + extends Asset.AssetBuilder {} + + /** + * Remove the system description from a APIMethod. + * + * @param client connectivity to the Atlan tenant on which to remove the asset's description + * @param qualifiedName of the APIMethod + * @param name of the APIMethod + * @return the updated APIMethod, or null if the removal failed + * @throws AtlanException on any API problems + */ + public static APIMethod removeDescription(AtlanClient client, String qualifiedName, String name) + throws AtlanException { + return (APIMethod) Asset.removeDescription(client, updater(qualifiedName, name)); + } + + /** + * Remove the user's description from a APIMethod. + * + * @param client connectivity to the Atlan tenant on which to remove the asset's description + * @param qualifiedName of the APIMethod + * @param name of the APIMethod + * @return the updated APIMethod, or null if the removal failed + * @throws AtlanException on any API problems + */ + public static APIMethod removeUserDescription(AtlanClient client, String qualifiedName, String name) + throws AtlanException { + return (APIMethod) Asset.removeUserDescription(client, updater(qualifiedName, name)); + } + + /** + * Remove the owners from a APIMethod. + * + * @param client connectivity to the Atlan client from which to remove the APIMethod's owners + * @param qualifiedName of the APIMethod + * @param name of the APIMethod + * @return the updated APIMethod, or null if the removal failed + * @throws AtlanException on any API problems + */ + public static APIMethod removeOwners(AtlanClient client, String qualifiedName, String name) throws AtlanException { + return (APIMethod) Asset.removeOwners(client, updater(qualifiedName, name)); + } + + /** + * Update the certificate on a APIMethod. + * + * @param client connectivity to the Atlan tenant on which to update the APIMethod's certificate + * @param qualifiedName of the APIMethod + * @param certificate to use + * @param message (optional) message, or null if no message + * @return the updated APIMethod, or null if the update failed + * @throws AtlanException on any API problems + */ + public static APIMethod updateCertificate( + AtlanClient client, String qualifiedName, CertificateStatus certificate, String message) + throws AtlanException { + return (APIMethod) Asset.updateCertificate(client, _internal(), TYPE_NAME, qualifiedName, certificate, message); + } + + /** + * Remove the certificate from a APIMethod. + * + * @param client connectivity to the Atlan tenant from which to remove the APIMethod's certificate + * @param qualifiedName of the APIMethod + * @param name of the APIMethod + * @return the updated APIMethod, or null if the removal failed + * @throws AtlanException on any API problems + */ + public static APIMethod removeCertificate(AtlanClient client, String qualifiedName, String name) + throws AtlanException { + return (APIMethod) Asset.removeCertificate(client, updater(qualifiedName, name)); + } + + /** + * Update the announcement on a APIMethod. + * + * @param client connectivity to the Atlan tenant on which to update the APIMethod's announcement + * @param qualifiedName of the APIMethod + * @param type type of announcement to set + * @param title (optional) title of the announcement to set (or null for no title) + * @param message (optional) message of the announcement to set (or null for no message) + * @return the result of the update, or null if the update failed + * @throws AtlanException on any API problems + */ + public static APIMethod updateAnnouncement( + AtlanClient client, String qualifiedName, AtlanAnnouncementType type, String title, String message) + throws AtlanException { + return (APIMethod) + Asset.updateAnnouncement(client, _internal(), TYPE_NAME, qualifiedName, type, title, message); + } + + /** + * Remove the announcement from a APIMethod. + * + * @param client connectivity to the Atlan client from which to remove the APIMethod's announcement + * @param qualifiedName of the APIMethod + * @param name of the APIMethod + * @return the updated APIMethod, or null if the removal failed + * @throws AtlanException on any API problems + */ + public static APIMethod removeAnnouncement(AtlanClient client, String qualifiedName, String name) + throws AtlanException { + return (APIMethod) Asset.removeAnnouncement(client, updater(qualifiedName, name)); + } + + /** + * Replace the terms linked to the APIMethod. + * + * @param client connectivity to the Atlan tenant on which to replace the APIMethod's assigned terms + * @param qualifiedName for the APIMethod + * @param name human-readable name of the APIMethod + * @param terms the list of terms to replace on the APIMethod, or null to remove all terms from the APIMethod + * @return the APIMethod that was updated (note that it will NOT contain details of the replaced terms) + * @throws AtlanException on any API problems + */ + public static APIMethod replaceTerms( + AtlanClient client, String qualifiedName, String name, List terms) throws AtlanException { + return (APIMethod) Asset.replaceTerms(client, updater(qualifiedName, name), terms); + } + + /** + * Link additional terms to the APIMethod, without replacing existing terms linked to the APIMethod. + * Note: this operation must make two API calls — one to retrieve the APIMethod's existing terms, + * and a second to append the new terms. + * + * @param client connectivity to the Atlan tenant on which to append terms to the APIMethod + * @param qualifiedName for the APIMethod + * @param terms the list of terms to append to the APIMethod + * @return the APIMethod that was updated (note that it will NOT contain details of the appended terms) + * @throws AtlanException on any API problems + * @deprecated see {@link com.atlan.model.assets.Asset.AssetBuilder#appendAssignedTerm(GlossaryTerm)} + */ + @Deprecated + public static APIMethod appendTerms(AtlanClient client, String qualifiedName, List terms) + throws AtlanException { + return (APIMethod) Asset.appendTerms(client, TYPE_NAME, qualifiedName, terms); + } + + /** + * Remove terms from a APIMethod, without replacing all existing terms linked to the APIMethod. + * Note: this operation must make two API calls — one to retrieve the APIMethod's existing terms, + * and a second to remove the provided terms. + * + * @param client connectivity to the Atlan tenant from which to remove terms from the APIMethod + * @param qualifiedName for the APIMethod + * @param terms the list of terms to remove from the APIMethod, which must be referenced by GUID + * @return the APIMethod that was updated (note that it will NOT contain details of the resulting terms) + * @throws AtlanException on any API problems + * @deprecated see {@link com.atlan.model.assets.Asset.AssetBuilder#removeAssignedTerm(GlossaryTerm)} + */ + @Deprecated + public static APIMethod removeTerms(AtlanClient client, String qualifiedName, List terms) + throws AtlanException { + return (APIMethod) Asset.removeTerms(client, TYPE_NAME, qualifiedName, terms); + } + + /** + * Add Atlan tags to a APIMethod, without replacing existing Atlan tags linked to the APIMethod. + * Note: this operation must make two API calls — one to retrieve the APIMethod's existing Atlan tags, + * and a second to append the new Atlan tags. + * + * @param client connectivity to the Atlan tenant on which to append Atlan tags to the APIMethod + * @param qualifiedName of the APIMethod + * @param atlanTagNames human-readable names of the Atlan tags to add + * @throws AtlanException on any API problems + * @return the updated APIMethod + * @deprecated see {@link com.atlan.model.assets.Asset.AssetBuilder#appendAtlanTags(List)} + */ + @Deprecated + public static APIMethod appendAtlanTags(AtlanClient client, String qualifiedName, List atlanTagNames) + throws AtlanException { + return (APIMethod) Asset.appendAtlanTags(client, TYPE_NAME, qualifiedName, atlanTagNames); + } + + /** + * Add Atlan tags to a APIMethod, without replacing existing Atlan tags linked to the APIMethod. + * Note: this operation must make two API calls — one to retrieve the APIMethod's existing Atlan tags, + * and a second to append the new Atlan tags. + * + * @param client connectivity to the Atlan tenant on which to append Atlan tags to the APIMethod + * @param qualifiedName of the APIMethod + * @param atlanTagNames human-readable names of the Atlan tags to add + * @param propagate whether to propagate the Atlan tag (true) or not (false) + * @param removePropagationsOnDelete whether to remove the propagated Atlan tags when the Atlan tag is removed from this asset (true) or not (false) + * @param restrictLineagePropagation whether to avoid propagating through lineage (true) or do propagate through lineage (false) + * @throws AtlanException on any API problems + * @return the updated APIMethod + * @deprecated see {@link com.atlan.model.assets.Asset.AssetBuilder#appendAtlanTags(List, boolean, boolean, boolean, boolean)} + */ + @Deprecated + public static APIMethod appendAtlanTags( + AtlanClient client, + String qualifiedName, + List atlanTagNames, + boolean propagate, + boolean removePropagationsOnDelete, + boolean restrictLineagePropagation) + throws AtlanException { + return (APIMethod) Asset.appendAtlanTags( + client, + TYPE_NAME, + qualifiedName, + atlanTagNames, + propagate, + removePropagationsOnDelete, + restrictLineagePropagation); + } + + /** + * Remove an Atlan tag from a APIMethod. + * + * @param client connectivity to the Atlan tenant from which to remove an Atlan tag from a APIMethod + * @param qualifiedName of the APIMethod + * @param atlanTagName human-readable name of the Atlan tag to remove + * @throws AtlanException on any API problems, or if the Atlan tag does not exist on the APIMethod + * @deprecated see {@link com.atlan.model.assets.Asset.AssetBuilder#removeAtlanTag(String)} + */ + @Deprecated + public static void removeAtlanTag(AtlanClient client, String qualifiedName, String atlanTagName) + throws AtlanException { + Asset.removeAtlanTag(client, TYPE_NAME, qualifiedName, atlanTagName); + } +} diff --git a/sdk/src/main/java/com/atlan/model/assets/APIObject.java b/sdk/src/main/java/com/atlan/model/assets/APIObject.java index dff54dfc54..c1661f01bd 100644 --- a/sdk/src/main/java/com/atlan/model/assets/APIObject.java +++ b/sdk/src/main/java/com/atlan/model/assets/APIObject.java @@ -66,6 +66,16 @@ public class APIObject extends Asset implements IAPIObject, IAPI, ICatalog, IAss @Attribute Boolean apiIsAuthOptional; + /** API methods that use this object as their request schema. */ + @Attribute + @Singular("apiMethodRequestingThis") + SortedSet apiMethodsRequestingThis; + + /** API methods that use this object as one of their response schemas. */ + @Attribute + @Singular("apiMethodRespondingWithThis") + SortedSet apiMethodsRespondingWithThis; + /** If this asset refers to an APIObject */ @Attribute Boolean apiIsObjectReference; diff --git a/sdk/src/main/java/com/atlan/model/assets/APIPath.java b/sdk/src/main/java/com/atlan/model/assets/APIPath.java index cbd4373f76..af3e428d58 100644 --- a/sdk/src/main/java/com/atlan/model/assets/APIPath.java +++ b/sdk/src/main/java/com/atlan/model/assets/APIPath.java @@ -65,6 +65,11 @@ public class APIPath extends Asset implements IAPIPath, IAPI, ICatalog, IAsset, @Attribute String apiObjectQualifiedName; + /** API methods (operations) available on this path. */ + @Attribute + @Singular + SortedSet apiMethods; + /** List of the operations available on the endpoint. */ @Attribute @Singular diff --git a/sdk/src/main/java/com/atlan/model/assets/IAPIMethod.java b/sdk/src/main/java/com/atlan/model/assets/IAPIMethod.java new file mode 100644 index 0000000000..4b1bd85277 --- /dev/null +++ b/sdk/src/main/java/com/atlan/model/assets/IAPIMethod.java @@ -0,0 +1,68 @@ +/* SPDX-License-Identifier: Apache-2.0 + Copyright 2023 Atlan Pte. Ltd. */ +package com.atlan.model.assets; + +import com.atlan.model.fields.KeywordField; +import com.atlan.model.fields.RelationField; +import com.atlan.model.fields.TextField; +import com.atlan.serde.AssetDeserializer; +import com.atlan.serde.AssetSerializer; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import java.util.Map; +import java.util.SortedSet; +import javax.annotation.processing.Generated; + +/** + * Instance of an API method (operation) on a path in Atlan. + * Represents a single HTTP method such as GET, POST, PUT, DELETE on an APIPath. + */ +@Generated(value = "com.atlan.generators.ModelGeneratorV2") +@JsonSerialize(using = AssetSerializer.class) +@JsonDeserialize(using = AssetDeserializer.class) +public interface IAPIMethod { + + public static final String TYPE_NAME = "APIMethod"; + + /** Request body or schema information for this API method. */ + TextField API_METHOD_REQUEST = new TextField("apiMethodRequest", "apiMethodRequest"); + + /** Response body or schema information for this API method. */ + TextField API_METHOD_RESPONSE = new TextField("apiMethodResponse", "apiMethodResponse"); + + /** Map of HTTP response status codes to the qualified names of the APIObject schemas that describe each response. */ + KeywordField API_METHOD_RESPONSE_CODES = new KeywordField("apiMethodResponseCodes", "apiMethodResponseCodes"); + + /** APIObject schema describing this method's request body. */ + RelationField API_METHOD_REQUEST_SCHEMA = new RelationField("apiMethodRequestSchema"); + + /** APIObject schemas describing this method's response bodies. */ + RelationField API_METHOD_RESPONSE_SCHEMAS = new RelationField("apiMethodResponseSchemas"); + + /** API path on which this method operates. */ + RelationField API_PATH = new RelationField("apiPath"); + + /** Request body or schema information for this API method. */ + String getApiMethodRequest(); + + /** Response body or schema information for this API method. */ + String getApiMethodResponse(); + + /** Map of HTTP response status codes to the qualified names of the APIObject schemas that describe each response. */ + Map getApiMethodResponseCodes(); + + /** APIObject schema describing this method's request body. */ + default IAPIObject getApiMethodRequestSchema() { + return null; + } + + /** APIObject schemas describing this method's response bodies. */ + default SortedSet getApiMethodResponseSchemas() { + return null; + } + + /** API path on which this method operates. */ + default IAPIPath getApiPath() { + return null; + } +} diff --git a/sdk/src/main/java/com/atlan/model/assets/IAPIObject.java b/sdk/src/main/java/com/atlan/model/assets/IAPIObject.java index 3ce4a6e3e9..c8bcae0284 100644 --- a/sdk/src/main/java/com/atlan/model/assets/IAPIObject.java +++ b/sdk/src/main/java/com/atlan/model/assets/IAPIObject.java @@ -44,6 +44,12 @@ public interface IAPIObject { /** Count of the APIField of this object. */ NumericField API_FIELD_COUNT = new NumericField("apiFieldCount", "apiFieldCount"); + /** API methods that use this object as their request schema. */ + RelationField API_METHODS_REQUESTING_THIS = new RelationField("apiMethodsRequestingThis"); + + /** API methods that use this object as one of their response schemas. */ + RelationField API_METHODS_RESPONDING_WITH_THIS = new RelationField("apiMethodsRespondingWithThis"); + /** APIField assets contained within this APIObject. */ RelationField API_FIELDS = new RelationField("apiFields"); @@ -90,6 +96,16 @@ default SortedSet getApiFields() { /** Whether authentication is optional (true) or required (false). */ Boolean getApiIsAuthOptional(); + /** API methods that use this object as their request schema. */ + default SortedSet getApiMethodsRequestingThis() { + return null; + } + + /** API methods that use this object as one of their response schemas. */ + default SortedSet getApiMethodsRespondingWithThis() { + return null; + } + /** If this asset refers to an APIObject */ Boolean getApiIsObjectReference(); diff --git a/sdk/src/main/java/com/atlan/model/assets/IAPIPath.java b/sdk/src/main/java/com/atlan/model/assets/IAPIPath.java index 89ab7ba12b..a04797ed05 100644 --- a/sdk/src/main/java/com/atlan/model/assets/IAPIPath.java +++ b/sdk/src/main/java/com/atlan/model/assets/IAPIPath.java @@ -64,6 +64,9 @@ public interface IAPIPath { /** Descriptive summary intended to apply to all operations in this path. */ TextField API_PATH_SUMMARY = new TextField("apiPathSummary", "apiPathSummary"); + /** API methods (operations) available on this path. */ + RelationField API_METHODS = new RelationField("apiMethods"); + /** API specification in which this path exists. */ RelationField API_SPEC = new RelationField("apiSpec"); @@ -108,6 +111,11 @@ default SortedSet getAnomaloChecks() { /** Qualified name of the APIObject that is referred to by this asset. When apiIsObjectReference is true. */ String getApiObjectQualifiedName(); + /** API methods (operations) available on this path. */ + default SortedSet getApiMethods() { + return null; + } + /** List of the operations available on the endpoint. */ SortedSet getApiPathAvailableOperations(); diff --git a/sdk/src/main/java/com/atlan/model/assets/_overlays/APIMethod.java b/sdk/src/main/java/com/atlan/model/assets/_overlays/APIMethod.java new file mode 100644 index 0000000000..a09f746f5f --- /dev/null +++ b/sdk/src/main/java/com/atlan/model/assets/_overlays/APIMethod.java @@ -0,0 +1,34 @@ + /** + * Builds the minimal object necessary to create an API method. + * + * @param httpMethod the HTTP method (e.g. GET, POST, PUT, DELETE) for this API method + * @param apiPath in which the API method should be created, which must have at least + * a qualifiedName + * @return the minimal request necessary to create the API method, as a builder + * @throws InvalidRequestException if the apiPath provided is without a qualifiedName + */ + public static APIMethodBuilder creator(String httpMethod, APIPath apiPath) throws InvalidRequestException { + Map map = new HashMap<>(); + map.put("qualifiedName", apiPath.getQualifiedName()); + validateRelationship(APIPath.TYPE_NAME, map); + return creator(httpMethod, apiPath.getQualifiedName()).apiPath(apiPath.trimToReference()); + } + + /** + * Builds the minimal object necessary to create an API method. + * + * @param httpMethod the HTTP method (e.g. GET, POST, PUT, DELETE) for this API method + * @param apiPathQualifiedName unique name of the API path on which this method operates + * @return the minimal object necessary to create the API method, as a builder + */ + public static APIMethodBuilder creator(String httpMethod, String apiPathQualifiedName) { + String connectionQualifiedName = StringUtils.getParentQualifiedNameFromQualifiedName( + StringUtils.getParentQualifiedNameFromQualifiedName(apiPathQualifiedName)); + String normalizedMethod = httpMethod.toUpperCase(); + return APIMethod._internal() + .guid("-" + ThreadLocalRandom.current().nextLong(0, Long.MAX_VALUE - 1)) + .qualifiedName(apiPathQualifiedName + "/" + normalizedMethod) + .name(normalizedMethod) + .apiPath(APIPath.refByQualifiedName(apiPathQualifiedName)) + .connectionQualifiedName(connectionQualifiedName); + }