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..fab86c6549 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,32 @@ 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.annotation.JsonInclude +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.io.OutputStreamWriter +import java.net.HttpURLConnection +import java.net.URL +import java.nio.file.Files 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().apply { + setSerializationInclusion(JsonInclude.Include.NON_NULL) + } + + /** Object creation mode: create physical per-method instances (default). */ + const val MODE_PHYSICAL = "PHYSICAL" + + /** Object creation mode: skip APIObject/APIField creation entirely (bodies-only). */ + const val MODE_NONE = "NONE" /** * Actually run the loader, taking all settings from environment variables. @@ -47,6 +67,9 @@ object OpenAPISpecLoader { exitProcess(4) } + val objectMode = ctx.config.objectCreationMode?.ifBlank { MODE_PHYSICAL } ?: MODE_PHYSICAL + logger.info { "Object creation mode: $objectMode" } + val sourceFiles = when (ctx.config.importType) { "DIRECT" -> { @@ -88,7 +111,7 @@ object OpenAPISpecLoader { } } for (sourceFile in sourceFiles) { - processFile(ctx, connectionQN, sourceFile, batchSize, outputDirectory) + processFile(ctx, connectionQN, sourceFile, batchSize, outputDirectory, objectMode) } } } @@ -99,6 +122,7 @@ object OpenAPISpecLoader { sourceFile: String, batchSize: Int, outputDirectory: String, + objectMode: String, ) { val fileType = Paths @@ -109,18 +133,24 @@ object OpenAPISpecLoader { when (fileType) { "json", "yaml", "yml" -> { logger.info { "Loading OpenAPI specification from $sourceFile into: $connectionQN" } - loadOpenAPISpec(ctx.client, connectionQN, OpenAPISpecReader(sourceFile), batchSize) + loadOpenAPISpec(ctx.client, connectionQN, OpenAPISpecReader(sourceFile), batchSize, objectMode) } "zip" -> { logger.info { "Extracting and processing ZIP file: $sourceFile" } val extractedFiles = Utils.unzipFiles(sourceFile, outputDirectory) - processExtractedFiles(ctx, connectionQN, extractedFiles, batchSize) + processExtractedFiles(ctx, connectionQN, extractedFiles, batchSize, objectMode) } else -> { - logger.error { "Invalid file type. Please provide a JSON, YAML or ZIP file." } - exitProcess(1) + // No extension — might be a URL + if (sourceFile.startsWith("http://") || sourceFile.startsWith("https://")) { + logger.info { "Loading OpenAPI specification from URL $sourceFile into: $connectionQN" } + loadOpenAPISpec(ctx.client, connectionQN, OpenAPISpecReader(sourceFile), batchSize, objectMode) + } else { + logger.error { "Invalid file type. Please provide a JSON, YAML or ZIP file." } + exitProcess(1) + } } } } @@ -130,10 +160,11 @@ object OpenAPISpecLoader { connectionQN: String, extractedFiles: List, batchSize: Int, + objectMode: String, ) { extractedFiles.filter { it.endsWith(".json") || it.endsWith(".yaml") || it.endsWith(".yml") }.forEach { file -> logger.info { "Loading OpenAPI specification from extracted file: $file" } - loadOpenAPISpec(ctx.client, connectionQN, OpenAPISpecReader(file), batchSize) + loadOpenAPISpec(ctx.client, connectionQN, OpenAPISpecReader(file), batchSize, objectMode) } } @@ -144,14 +175,17 @@ object OpenAPISpecLoader { * @param connectionQN qualifiedName of the connection in which to create the assets * @param spec object for reading from the OpenAPI spec itself * @param batchSize maximum number of assets to save per API request + * @param objectMode how to create APIObject/APIField assets: PHYSICAL (per-body instances) or NONE (skip) */ fun loadOpenAPISpec( client: AtlanClient, connectionQN: String, spec: OpenAPISpecReader, batchSize: Int, + objectMode: String = MODE_PHYSICAL, ) { - val toCreate = + // --- Step 1: Save the APISpec with raw content --- + val specBuilder = APISpec .creator(spec.title, connectionQN) .sourceURL(spec.sourceURL) @@ -166,7 +200,10 @@ object OpenAPISpecLoader { .apiSpecVersion(spec.version) .apiExternalDoc("url", spec.externalDocsURL) .apiExternalDoc("description", spec.externalDocsDescription) - .build() + if (spec.rawContent.isNotEmpty()) { + specBuilder.apiSpecRawContent(spec.rawContent) + } + val toCreate = specBuilder.build() val specQN = toCreate.qualifiedName logger.info { "Saving APISpec: $specQN" } try { @@ -186,41 +223,645 @@ object OpenAPISpecLoader { logger.error("Unable to save the APISpec.", e) exitProcess(5) } + + // --- Step 2: Create APIPaths and APIMethod per operation --- + // In PHYSICAL mode, APIObjects/APIFields are created per-body during method processing. + // In NONE mode, only paths and methods are created (bodies-only). + // Collect method-to-object relationship mappings (SDK attribute names don't match Atlas + // typedef end names, so we create these via the REST API after entity creation) + val methodRequestSchemaMap = mutableMapOf() // methodQN → requestObjectQN + val methodResponseSchemaMap = mutableMapOf>() // methodQN → [responseObjectQNs] + 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 { objectBatch -> + AssetBatch(client, batchSize, AtlanTagHandling.IGNORE, CustomMetadataHandling.MERGE, true).use { fieldBatch -> + try { + val assetCount = AtomicLong(0) + for (apiPath in spec.paths.entries) { + val pathUrl = apiPath.key + val pathDetails = apiPath.value + + // --- APIPath creation --- + 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) + // Encode path slashes as ~ so each hierarchy level = 1 QN segment + val encodedPath = pathUrl.replace("/", "~") + val pathQN = "$specQN/$encodedPath" + val path = + APIPath + ._internal() + .guid("-" + ThreadLocalRandom.current().nextLong(0, Long.MAX_VALUE - 1)) + .qualifiedName(pathQN) + .name(pathUrl) + .connectionQualifiedName(connectionQN) + .apiSpec(APISpec.refByQualifiedName(specQN)) + .apiSpecQualifiedName(specQN) + .apiPathQualifiedName(pathQN) + .description(desc.toString()) + .apiPathRawURI(pathUrl) + .apiPathSummary(pathDetails.summary) + .apiPathAvailableOperations(operations) + .apiPathIsTemplated(pathUrl.contains("{") && pathUrl.contains("}")) + .build() + pathBatch.add(path) + + // --- APIMethod creation per operation --- + 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, objectMode, methodBatch, objectBatch, fieldBatch, methodRequestSchemaMap, methodResponseSchemaMap) + } + + Utils.logProgress(assetCount, totalCount, logger, batchSize) + } + pathBatch.flush() + objectBatch.flush() + fieldBatch.flush() + methodBatch.flush() + Utils.logProgress(assetCount, totalCount, logger, batchSize) + } catch (e: AtlanException) { + logger.error("Unable to bulk-save API paths/methods.", e) + } + } + } + } + } + } + + // --- Step 3: Create method↔object relationships via REST API --- + // The SDK's relationship attribute names for method-object peer relationships are + // swapped relative to the Atlas typedef end names, so we create these relationships + // directly via the Atlas relationship REST API using qualified name references. + if (objectMode == MODE_PHYSICAL) { + val baseUrl = client.baseUrl + val apiToken = System.getenv("ATLAN_API_KEY") ?: "" + logger.info { "Creating method↔object relationships (${methodRequestSchemaMap.size} request, ${methodResponseSchemaMap.values.sumOf { it.size }} response)..." } + + for ((methodQN, objectQN) in methodRequestSchemaMap) { + createRelationshipViaApi( + baseUrl, apiToken, + "api_method_request_schema_api_methods_requesting_this", + methodQN, "APIMethod", objectQN, "APIObject", + ) + } + for ((methodQN, objectQNs) in methodResponseSchemaMap) { + for (objectQN in objectQNs) { + createRelationshipViaApi( + baseUrl, apiToken, + "api_method_response_schemas_api_methods_responding_with_this", + methodQN, "APIMethod", objectQN, "APIObject", + ) + } + } + logger.info { "Relationships created." } + } + } + + /** + * Create an APIMethod asset for a single HTTP operation on a path, if the operation exists. + * In PHYSICAL mode, also creates contextualized APIObject/APIField instances for each body. + * In NONE mode, only sets the JSON blob attributes on the method (no APIObject/APIField). + */ + private fun createMethodIfPresent( + operation: Operation?, + httpMethod: String, + pathQN: String, + pathUrl: String, + specQN: String, + connectionQN: String, + spec: OpenAPISpecReader, + objectMode: String, + methodBatch: AssetBatch, + objectBatch: AssetBatch, + fieldBatch: AssetBatch, + methodRequestSchemaMap: MutableMap, + methodResponseSchemaMap: MutableMap>, + ) { + if (operation == null) return + + val methodName = "$httpMethod $pathUrl" + val methodQN = "$pathQN/$httpMethod" + val methodContext = "${httpMethod}_${sanitizePath(pathUrl)}" + + val methodBuilder = + APIMethod + ._internal() + .guid("-" + ThreadLocalRandom.current().nextLong(0, Long.MAX_VALUE - 1)) + .qualifiedName(methodQN) + .name(methodName) + .connectionQualifiedName(connectionQN) + .apiSpecQualifiedName(specQN) + .apiPathQualifiedName(pathQN) + .apiMethodQualifiedName(methodQN) + .apiSpecName(spec.title) + .apiSpecType(spec.openAPIVersion) + .description(operation.summary ?: operation.description ?: "") + .apiPath(APIPath.refByQualifiedName(pathQN)) + + // --- Request body --- + val pathQNForHierarchy = pathQN + val requestSchema = extractRequestSchema(operation) + if (requestSchema != null) { + val requestExample = generateExample(requestSchema, spec.schemas) + methodBuilder.apiMethodRequest( + if (requestExample != null) jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(requestExample) + else serializeSchema(requestSchema, spec.schemas) + ) + + if (objectMode == MODE_PHYSICAL) { + val resolvedSchema = resolveSchema(requestSchema, spec.schemas) + if (resolvedSchema != null && hasProperties(resolvedSchema)) { + // Object schema with properties → full APIObject + child APIFields + val schemaName = extractRefSchemaName(requestSchema) ?: "${methodContext}_Request" + val physicalName = "${methodContext}/request/$schemaName" + val physicalQN = createPhysicalSchemaObject( + physicalName, "request", schemaName, resolvedSchema, + specQN, connectionQN, pathQNForHierarchy, methodQN, "request", + spec, objectBatch, fieldBatch, mutableSetOf(schemaName), + ) + methodRequestSchemaMap[methodQN] = physicalQN + } else { + // Primitive or unresolved schema → shell APIObject (no child fields) + val schemaLabel = requestSchema.type ?: "inline" + val physicalName = "${methodContext}/request/$schemaLabel" + val objectQN = createShellResponseObject( + physicalName, "request", "request", + specQN, connectionQN, pathQNForHierarchy, methodQN, + spec, objectBatch, + ) + methodRequestSchemaMap[methodQN] = objectQN + } + } + } + + // --- Response bodies --- + val responseCodes = mutableMapOf() + val responseSchemas = mutableListOf() + val allResponseBlobs = mutableMapOf() + if (operation.responses != null) { + for ((statusCode, apiResponse) in operation.responses) { + val description = apiResponse.description ?: "" + val responseSchema = extractResponseSchema(apiResponse) + val bodyType = "response/$statusCode" + val bodyDisplayName = "response $statusCode" + + if (responseSchema != null) { + val blobEntry = mutableMapOf() + blobEntry["description"] = description + val responseExample = generateExample(responseSchema, spec.schemas) + if (responseExample != null) { + blobEntry["example"] = responseExample + } + + if (objectMode == MODE_PHYSICAL) { + val resolvedSchema = resolveSchema(responseSchema, spec.schemas) + if (resolvedSchema != null && hasProperties(resolvedSchema)) { + // Object schema with properties → full APIObject + child APIFields + val schemaName = extractRefSchemaName(responseSchema) ?: "${methodContext}_${statusCode}_Response" + val physicalName = "${methodContext}/response/${statusCode}/$schemaName" + val physicalQN = createPhysicalSchemaObject( + physicalName, bodyDisplayName, schemaName, resolvedSchema, + specQN, connectionQN, pathQNForHierarchy, methodQN, bodyType, + spec, objectBatch, fieldBatch, mutableSetOf(schemaName), + ) + responseCodes[statusCode] = physicalQN + responseSchemas.add(physicalQN) + } else { + // Primitive or unresolved schema → shell APIObject (no child fields) + val schemaLabel = responseSchema.type ?: "inline" + val physicalName = "${methodContext}/response/${statusCode}/$schemaLabel" + val objectQN = createShellResponseObject( + physicalName, bodyDisplayName, bodyType, + specQN, connectionQN, pathQNForHierarchy, methodQN, + spec, objectBatch, + ) + responseCodes[statusCode] = objectQN + responseSchemas.add(objectQN) + } + } else { + // NONE mode — no objects, just record a label + val inlineName = "${methodContext}_$statusCode" + responseCodes[statusCode] = inlineName + } + allResponseBlobs[statusCode] = blobEntry + } else { + // No content block — description-only response + allResponseBlobs[statusCode] = mapOf("description" to description) + + if (objectMode == MODE_PHYSICAL) { + // Still create a shell APIObject so it appears in the hierarchy + val physicalName = "${methodContext}/response/${statusCode}/no_content" + val objectQN = createShellResponseObject( + physicalName, bodyDisplayName, bodyType, + specQN, connectionQN, pathQNForHierarchy, methodQN, + spec, objectBatch, + ) + responseCodes[statusCode] = objectQN + responseSchemas.add(objectQN) + } + } + } + } + if (allResponseBlobs.isNotEmpty()) { + methodBuilder.apiMethodResponse(jsonMapper.writeValueAsString(allResponseBlobs)) + } + if (responseCodes.isNotEmpty()) { + methodBuilder.apiMethodResponseCodes(responseCodes) + } + // Collect response schema relationships (created via REST API after batch flush) + if (responseSchemas.isNotEmpty()) { + methodResponseSchemaMap.getOrPut(methodQN) { mutableListOf() }.addAll(responseSchemas) + } + + methodBatch.add(methodBuilder.build()) + } + + /** + * Create a physical (per-body) APIObject and its APIFields for a schema occurrence, + * recursing into nested $ref sub-objects. Names use xpath-style paths (e.g., "Pet/category/name"). + * + * @param contextPath QN path fragment like "POST_pet/request/Pet" that makes this instance unique + * @param displayName xpath-style display name for the object (e.g., "Pet" or "Pet/category") + * @param rootSchemaName the top-level schema name, used as prefix for all child names + * @param schema the resolved schema with properties + * @param specQN qualified name of the parent APISpec + * @param connectionQN qualified name of the connection + * @param pathQN qualified name of the parent APIPath (for hierarchy filters) + * @param methodQN qualified name of the parent APIMethod (for hierarchy filters) + * @param bodyType body context: "request", "response/200", etc. (for body filter) + * @param spec the OpenAPISpecReader for spec-level metadata + * @param objectBatch the AssetBatch for APIObjects + * @param fieldBatch the AssetBatch for APIFields + * @param visitedSchemas set of schema names visited in the current recursion path (cycle detection) + * @return the qualified name of the created APIObject + */ + private fun createPhysicalSchemaObject( + contextPath: String, + displayName: String, + rootSchemaName: String, + schema: Schema<*>, + specQN: String, + connectionQN: String, + pathQN: String, + methodQN: String, + bodyType: String, + spec: OpenAPISpecReader, + objectBatch: AssetBatch, + fieldBatch: AssetBatch, + visitedSchemas: MutableSet, + ): String { + val objectQN = "$specQN/schemas/$contextPath" + val properties = schema.properties ?: emptyMap() + val apiObject = + APIObject + ._internal() + .guid("-" + ThreadLocalRandom.current().nextLong(0, Long.MAX_VALUE - 1)) + .qualifiedName(objectQN) + .name(displayName) + .connectionQualifiedName(connectionQN) + .apiSpecQualifiedName(specQN) + .apiSpecName(spec.title) + .apiSpecType(spec.openAPIVersion) + .apiFieldCount(properties.size.toLong()) + .apiPathQualifiedName(pathQN) + .apiMethodQualifiedName(methodQN) + .apiBodyType(bodyType) + .build() + objectBatch.add(apiObject) + + for ((fieldName, fieldSchema) in properties) { + val isArray = fieldSchema.type == "array" + val arraySuffix = if (isArray) "[]" else "" + val fieldDisplayName = "$displayName/$fieldName$arraySuffix" + val fieldQN = "$objectQN/$fieldName$arraySuffix" + + 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(fieldDisplayName) + .connectionQualifiedName(connectionQN) + .apiSpecQualifiedName(specQN) + .apiSpecName(spec.title) + .apiSpecType(spec.openAPIVersion) + .apiFieldType(fieldType) + .apiObject(APIObject.refByQualifiedName(objectQN)) + .apiPathQualifiedName(pathQN) + .apiMethodQualifiedName(methodQN) + .apiBodyType(bodyType) + if (fieldTypeSecondary != null) { + fieldBuilder.apiFieldTypeSecondary(fieldTypeSecondary) + } + // Set description from the spec definition (always set to clear old values) + fieldBuilder.description(fieldSchema.description ?: "") + + if (isObjectRef) { + fieldBuilder.apiIsObjectReference(true) + + // Recurse into sub-objects if not a cycle + if (refSchemaName != null && refSchemaName !in visitedSchemas) { + val resolvedSubSchema = resolveSchema(fieldSchema, spec.schemas) + if (resolvedSubSchema != null && hasProperties(resolvedSubSchema)) { + visitedSchemas.add(refSchemaName) + val childContextPath = "$contextPath/$fieldName$arraySuffix" + val childQN = createPhysicalSchemaObject( + childContextPath, fieldDisplayName, rootSchemaName, resolvedSubSchema, + specQN, connectionQN, pathQN, methodQN, bodyType, + spec, objectBatch, fieldBatch, visitedSchemas, + ) + fieldBuilder.apiObjectQualifiedName(childQN) + visitedSchemas.remove(refSchemaName) + } + } + } + fieldBatch.add(fieldBuilder.build()) + } + return objectQN + } + + /** + * Create a shell APIObject for a response that has no object-type schema + * (primitive type, no content, or unresolved). This ensures every defined + * response appears in the hierarchy as a governable body entity. + * + * @return the qualified name of the created APIObject + */ + private fun createShellResponseObject( + contextPath: String, + displayName: String, + bodyType: String, + specQN: String, + connectionQN: String, + pathQN: String, + methodQN: String, + spec: OpenAPISpecReader, + objectBatch: AssetBatch, + ): String { + val objectQN = "$specQN/schemas/$contextPath" + val apiObject = + APIObject + ._internal() + .guid("-" + ThreadLocalRandom.current().nextLong(0, Long.MAX_VALUE - 1)) + .qualifiedName(objectQN) + .name(displayName) + .connectionQualifiedName(connectionQN) + .apiSpecQualifiedName(specQN) + .apiSpecName(spec.title) + .apiSpecType(spec.openAPIVersion) + .apiFieldCount(0L) + .apiPathQualifiedName(pathQN) + .apiMethodQualifiedName(methodQN) + .apiBodyType(bodyType) + .build() + objectBatch.add(apiObject) + return objectQN + } + + /** + * Resolve a schema that may be a $ref to its full definition from components/schemas. + * Returns null if the schema cannot be resolved. + */ + private fun resolveSchema( + schema: Schema<*>, + schemas: Map>?, + ): Schema<*>? { + val refName = extractRefSchemaName(schema) + if (refName != null) { + return schemas?.get(refName) + } + // If it's an array with $ref items, resolve the item schema + if (schema.type == "array" && schema.items != null) { + val itemRefName = extractRefSchemaName(schema.items) + if (itemRefName != null) { + return schemas?.get(itemRefName) + } + } + // Inline schema — return it directly if it has properties + return schema + } + + /** + * Check whether a schema has properties worth creating an APIObject for. + */ + private fun hasProperties(schema: Schema<*>): Boolean = + schema.properties != null && schema.properties.isNotEmpty() + + /** + * 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 (schema.`$ref` != null) return "object" + return schema.type ?: "object" + } + + /** + * 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") { + return "array" + } + return null + } + + /** + * Serialize a schema to a JSON string for the blob attributes. + * Resolves $ref references and omits null values for cleaner output. + */ + private fun serializeSchema(schema: Schema<*>, schemas: Map>? = null): String = + try { + val resolved = if (schemas != null) resolveSchema(schema, schemas) ?: schema else schema + jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(resolved) + } catch (e: Exception) { + schema.toString() + } + + /** + * Generate an example payload from a schema definition. + * Uses the schema's `example` field when available, otherwise generates + * a representative value based on type and format. + * Arrays are limited to 1 item (or minItems if specified). + */ + private fun generateExample( + schema: Schema<*>, + schemas: Map>?, + visited: MutableSet = mutableSetOf(), + ): Any? { + // Resolve $ref + val refName = extractRefSchemaName(schema) + if (refName != null) { + if (refName in visited) return emptyMap() // cycle guard + visited.add(refName) + val resolved = schemas?.get(refName) + val result = if (resolved != null) generateExample(resolved, schemas, visited) else null + visited.remove(refName) + return result + } + + // Schema-level example takes priority + if (schema.example != null) return schema.example + + // Array → generate 1 item (or minItems) + if (schema.type == "array" && schema.items != null) { + val itemExample = generateExample(schema.items, schemas, visited) + val count = schema.minItems ?: 1 + return (1..count).map { itemExample } + } + + // Object with properties + if (schema.properties != null && schema.properties.isNotEmpty()) { + val obj = linkedMapOf() + for ((name, propSchema) in schema.properties) { + obj[name] = generateExample(propSchema, schemas, visited) + } + return obj + } + + // Primitive + return generatePrimitiveExample(schema) + } + + /** + * Generate a representative primitive value based on type and format. + */ + private fun generatePrimitiveExample(schema: Schema<*>): Any { + if (schema.example != null) return schema.example + + // Enum: pick first value + if (schema.enum != null && schema.enum.isNotEmpty()) return schema.enum[0] + + return when (schema.type) { + "string" -> when (schema.format) { + "date-time" -> "2024-01-15T09:30:00Z" + "date" -> "2024-01-15" + "email" -> "user@example.com" + "uri", "url" -> "https://example.com" + "uuid" -> "550e8400-e29b-41d4-a716-446655440000" + "byte" -> "dGVzdA==" + "binary" -> "" + "password" -> "********" + else -> "string" + } + "integer", "int" -> when (schema.format) { + "int64" -> 100000L + else -> 10 + } + "number" -> when (schema.format) { + "float" -> 3.14 + "double" -> 3.14159 + else -> 1.5 + } + "boolean" -> true + else -> "value" + } + } + + /** + * Create a relationship between two entities via the Atlas REST API. + * This bypasses the SDK's serialization which has swapped attribute names + * for the method↔object peer relationships. + */ + private fun createRelationshipViaApi( + baseUrl: String, + apiToken: String, + relationshipTypeName: String, + end1QN: String, + end1Type: String, + end2QN: String, + end2Type: String, + ) { + try { + val payload = """ + { + "typeName": "$relationshipTypeName", + "end1": { + "typeName": "$end1Type", + "uniqueAttributes": { "qualifiedName": "$end1QN" } + }, + "end2": { + "typeName": "$end2Type", + "uniqueAttributes": { "qualifiedName": "$end2QN" } } - batch.flush() - Utils.logProgress(assetCount, totalCount, logger, batchSize) - } catch (e: AtlanException) { - logger.error("Unable to bulk-save API paths.", e) } + """.trimIndent() + val url = URL("$baseUrl/api/meta/relationship") + val conn = url.openConnection() as HttpURLConnection + conn.requestMethod = "POST" + conn.setRequestProperty("Authorization", "Bearer $apiToken") + conn.setRequestProperty("Content-Type", "application/json") + conn.doOutput = true + OutputStreamWriter(conn.outputStream).use { it.write(payload) } + val responseCode = conn.responseCode + if (responseCode !in 200..299) { + val error = conn.errorStream?.bufferedReader()?.readText() ?: "" + logger.warn { "Failed to create relationship $relationshipTypeName ($end1QN → $end2QN): HTTP $responseCode — $error" } } + conn.disconnect() + } catch (e: Exception) { + logger.warn { "Error creating relationship $relationshipTypeName ($end1QN → $end2QN): ${e.message}" } } } @@ -251,7 +892,7 @@ object OpenAPISpecLoader { /** * Utility class for parsing and reading the contents of an OpenAPI spec file, - * using the Swagger parser. + * using the Swagger parser. Also captures the raw file content for storage. */ class OpenAPISpecReader( url: String, @@ -261,6 +902,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 @@ -272,12 +914,14 @@ object OpenAPISpecLoader { val licenseURL: String val externalDocsURL: String val externalDocsDescription: String + val rawContent: String init { spec = OpenAPIV3Parser().read(url) sourceURL = url openAPIVersion = spec.openapi paths = spec.paths + schemas = spec.components?.schemas title = spec.info?.title ?: "" description = spec.info?.description ?: "" termsOfServiceURL = spec.info?.termsOfService ?: "" @@ -289,6 +933,17 @@ object OpenAPISpecLoader { licenseURL = spec.info?.license?.url ?: "" externalDocsURL = spec.externalDocs?.url ?: "" externalDocsDescription = spec.externalDocs?.description ?: "" + // Capture raw content of the spec file + rawContent = try { + if (url.startsWith("http://") || url.startsWith("https://")) { + URL(url).readText() + } else { + Files.readString(Paths.get(url)) + } + } catch (e: Exception) { + logger.warn { "Could not read raw spec content from $url: ${e.message}" } + "" + } } } } diff --git a/samples/packages/openapi-spec-loader/src/main/kotlin/OpenAPISpecLoaderCfg.kt b/samples/packages/openapi-spec-loader/src/main/kotlin/OpenAPISpecLoaderCfg.kt index 98f4020685..f85d2602e1 100644 --- a/samples/packages/openapi-spec-loader/src/main/kotlin/OpenAPISpecLoaderCfg.kt +++ b/samples/packages/openapi-spec-loader/src/main/kotlin/OpenAPISpecLoaderCfg.kt @@ -28,4 +28,5 @@ data class OpenAPISpecLoaderCfg( @JsonDeserialize(using = WidgetSerde.MultiSelectDeserializer::class) @JsonSerialize(using = WidgetSerde.MultiSelectSerializer::class) @JsonProperty("connection_qualified_name") val connectionQualifiedName: List? = null, + @JsonProperty("object_creation_mode") val objectCreationMode: String = "PHYSICAL", ) : CustomConfig() diff --git a/samples/packages/openapi-spec-loader/src/main/resources/package.pkl b/samples/packages/openapi-spec-loader/src/main/resources/package.pkl index 23c85f0d83..eefe11dbb5 100644 --- a/samples/packages/openapi-spec-loader/src/main/resources/package.pkl +++ b/samples/packages/openapi-spec-loader/src/main/resources/package.pkl @@ -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 { 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/APIField.java b/sdk/src/main/java/com/atlan/model/assets/APIField.java index cce762574a..e4e8274213 100644 --- a/sdk/src/main/java/com/atlan/model/assets/APIField.java +++ b/sdk/src/main/java/com/atlan/model/assets/APIField.java @@ -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 @@ -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; @@ -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; 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..600419799b --- /dev/null +++ b/sdk/src/main/java/com/atlan/model/assets/APIMethod.java @@ -0,0 +1,797 @@ +/* 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; + + /** Unique name of the API method in which this asset exists. */ + @Attribute + String apiMethodQualifiedName; + + /** Qualified name of the APIObject that is referred to by this asset. When apiIsObjectReference is true. */ + @Attribute + String apiObjectQualifiedName; + + /** Unique name of the API path in which this asset exists. */ + @Attribute + String apiPathQualifiedName; + + /** 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..f6be93a97c 100644 --- a/sdk/src/main/java/com/atlan/model/assets/APIObject.java +++ b/sdk/src/main/java/com/atlan/model/assets/APIObject.java @@ -48,6 +48,10 @@ public class APIObject extends Asset implements IAPIObject, IAPI, ICatalog, IAss @Builder.Default String typeName = TYPE_NAME; + /** Type of API body this object belongs to (e.g. "request", "response/200"). */ + @Attribute + String apiBodyType; + /** External documentation of the API. */ @Attribute @Singular @@ -66,6 +70,20 @@ 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; + + /** Qualified name of the API method this object belongs to. */ + @Attribute + String apiMethodQualifiedName; + + /** 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; @@ -74,6 +92,10 @@ public class APIObject extends Asset implements IAPIObject, IAPI, ICatalog, IAss @Attribute String apiObjectQualifiedName; + /** Qualified name of the API path this object belongs to. */ + @Attribute + String apiPathQualifiedName; + /** Simple name of the API spec, if this asset is contained in an API spec. */ @Attribute String apiSpecName; 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..bb7c8b96fc 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,15 @@ public class APIPath extends Asset implements IAPIPath, IAPI, ICatalog, IAsset, @Attribute String apiObjectQualifiedName; + /** Unique name of the API path in which this asset exists. */ + @Attribute + String apiPathQualifiedName; + + /** 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/APISpec.java b/sdk/src/main/java/com/atlan/model/assets/APISpec.java index c18f2424b9..908af3258a 100644 --- a/sdk/src/main/java/com/atlan/model/assets/APISpec.java +++ b/sdk/src/main/java/com/atlan/model/assets/APISpec.java @@ -86,6 +86,10 @@ public class APISpec extends Asset implements IAPISpec, IAPI, ICatalog, IAsset, @Attribute String apiSpecContractVersion; + /** Raw content of the API specification file (JSON or YAML). */ + @Attribute + String apiSpecRawContent; + /** Name of the license under which the API specification is available. */ @Attribute String apiSpecLicenseName; diff --git a/sdk/src/main/java/com/atlan/model/assets/IAPIField.java b/sdk/src/main/java/com/atlan/model/assets/IAPIField.java index eb1e0f29d0..2260995e63 100644 --- a/sdk/src/main/java/com/atlan/model/assets/IAPIField.java +++ b/sdk/src/main/java/com/atlan/model/assets/IAPIField.java @@ -42,6 +42,9 @@ public interface IAPIField { public static final String TYPE_NAME = "APIField"; + /** Type of API body this field belongs to (e.g. "request", "response/200"). */ + KeywordField API_BODY_TYPE = new KeywordField("apiBodyType", "apiBodyType"); + /** Type of APIField, as free text (e.g. STRING, NUMBER etc). */ KeywordField API_FIELD_TYPE = new KeywordField("apiFieldType", "apiFieldType"); @@ -51,6 +54,12 @@ public interface IAPIField { /** APIObject asset containing this APIField. */ RelationField API_OBJECT = new RelationField("apiObject"); + /** Qualified name of the API method this field belongs to. */ + KeywordField API_METHOD_QUALIFIED_NAME = new KeywordField("apiMethodQualifiedName", "apiMethodQualifiedName"); + + /** Qualified name of the API path this field belongs to. */ + KeywordField API_PATH_QUALIFIED_NAME = new KeywordField("apiPathQualifiedName", "apiPathQualifiedName"); + /** APIQuery asset containing this APIField. */ RelationField API_QUERY = new RelationField("apiQuery"); @@ -86,6 +95,9 @@ default SortedSet getAnomaloChecks() { return null; } + /** Type of API body this field belongs to (e.g. "request", "response/200"). */ + String getApiBodyType(); + /** External documentation of the API. */ Map getApiExternalDocs(); @@ -101,6 +113,9 @@ default SortedSet getAnomaloChecks() { /** If this asset refers to an APIObject */ Boolean getApiIsObjectReference(); + /** Qualified name of the API method this field belongs to. */ + String getApiMethodQualifiedName(); + /** APIObject asset containing this APIField. */ default IAPIObject getApiObject() { return null; @@ -109,6 +124,9 @@ default IAPIObject getApiObject() { /** Qualified name of the APIObject that is referred to by this asset. When apiIsObjectReference is true. */ String getApiObjectQualifiedName(); + /** Qualified name of the API path this field belongs to. */ + String getApiPathQualifiedName(); + /** APIQuery asset containing this APIField. */ default IAPIQuery getApiQuery() { return null; 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 8340d0ca41..b05bdae59a 100644 --- a/sdk/src/main/java/com/atlan/model/assets/IAPIObject.java +++ b/sdk/src/main/java/com/atlan/model/assets/IAPIObject.java @@ -13,6 +13,7 @@ import com.atlan.model.enums.DataQualityScheduleType; import com.atlan.model.enums.DataQualitySourceSyncStatus; import com.atlan.model.enums.SourceCostUnitType; +import com.atlan.model.fields.KeywordField; import com.atlan.model.fields.NumericField; import com.atlan.model.fields.RelationField; import com.atlan.model.relations.RelationshipAttributes; @@ -41,12 +42,27 @@ public interface IAPIObject { public static final String TYPE_NAME = "APIObject"; + /** Type of API body this object belongs to (e.g. "request", "response/200"). */ + KeywordField API_BODY_TYPE = new KeywordField("apiBodyType", "apiBodyType"); + /** 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"); + /** Qualified name of the API method this object belongs to. */ + KeywordField API_METHOD_QUALIFIED_NAME = new KeywordField("apiMethodQualifiedName", "apiMethodQualifiedName"); + + /** Qualified name of the API path this object belongs to. */ + KeywordField API_PATH_QUALIFIED_NAME = new KeywordField("apiPathQualifiedName", "apiPathQualifiedName"); + /** List of groups who administer this asset. (This is only used for certain asset types.) */ SortedSet getAdminGroups(); @@ -76,6 +92,9 @@ default SortedSet getAnomaloChecks() { return null; } + /** Type of API body this object belongs to (e.g. "request", "response/200"). */ + String getApiBodyType(); + /** External documentation of the API. */ Map getApiExternalDocs(); @@ -90,12 +109,28 @@ 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; + } + + /** Qualified name of the API method this object belongs to. */ + String getApiMethodQualifiedName(); + + /** 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(); /** Qualified name of the APIObject that is referred to by this asset. When apiIsObjectReference is true. */ String getApiObjectQualifiedName(); + /** Qualified name of the API path this object belongs to. */ + String getApiPathQualifiedName(); + /** Simple name of the API spec, if this asset is contained in an API spec. */ String getApiSpecName(); 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 26ef58de55..51293a2c1f 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/IAPISpec.java b/sdk/src/main/java/com/atlan/model/assets/IAPISpec.java index 739929cd55..9430ecd9e9 100644 --- a/sdk/src/main/java/com/atlan/model/assets/IAPISpec.java +++ b/sdk/src/main/java/com/atlan/model/assets/IAPISpec.java @@ -72,6 +72,9 @@ public interface IAPISpec { KeywordTextField API_SPEC_SERVICE_ALIAS = new KeywordTextField("apiSpecServiceAlias", "apiSpecServiceAlias", "apiSpecServiceAlias.text"); + /** Raw content of the API specification file (JSON or YAML). */ + KeywordField API_SPEC_RAW_CONTENT = new KeywordField("apiSpecRawContent", "apiSpecRawContent"); + /** URL to the terms of service for the API specification. */ KeywordTextField API_SPEC_TERMS_OF_SERVICE_URL = new KeywordTextField( "apiSpecTermsOfServiceURL", "apiSpecTermsOfServiceURL", "apiSpecTermsOfServiceURL.text"); 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); + }