Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds typedef toolkit attribute naming validation to avoid conflicts #875

Merged
merged 2 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 19 additions & 9 deletions typedef-toolkit/model/src/main/resources/Model.pkl
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,7 @@ const function getOutputs(m): Mapping<String, FileOutput> = new Mapping {
["models/\(m.shared.supertypeDefinition.name).json"] = getSupertypeJson(m)
when (m.customTypes != null) {
for (typeName, asset in m.customTypes) {
["models/\(m.shared.supertypeDefinition.name)/\(typeName).json"] = getCustomTypeJson(typeName, asset, m.shared.supertypeDefinition.name)
["models/\(m.shared.supertypeDefinition.name)/\(typeName).json"] = getCustomTypeJson(typeName, asset, m.shared.supertypeDefinition.name, m.customTypes)
}
}
["frontend/\(m.shared.supertypeDefinition.name)/src/constant/source/\(m.shared.supertypeDefinition.name.decapitalize())/common/index.ts"] = new FileOutput {
Expand Down Expand Up @@ -741,7 +741,7 @@ const function getOutputs(m): Mapping<String, FileOutput> = new Mapping {
renderer = new Renderers.UseBody { model = m }
}
for (typeName, asset in m.customTypes) {
["frontend/\(m.shared.supertypeDefinition.name)/src/api/schemas/metastore/atlas/entityDefs/\(m.shared.supertypeDefinition.name)/\(typeName).json"] = getCustomTypeJson(typeName, asset, m.shared.supertypeDefinition.name)
["frontend/\(m.shared.supertypeDefinition.name)/src/api/schemas/metastore/atlas/entityDefs/\(m.shared.supertypeDefinition.name)/\(typeName).json"] = getCustomTypeJson(typeName, asset, m.shared.supertypeDefinition.name, m.customTypes)
["frontend/\(m.shared.supertypeDefinition.name)/src/constant/source/\(m.shared.supertypeDefinition.name.decapitalize())/attributes/\(typeName.decapitalize()).ts"] = new FileOutput {
renderer = new Renderers.Attributes {
model = m
Expand Down Expand Up @@ -769,14 +769,15 @@ const function getSupertypeJson(m): FileOutput = new FileOutput {
/// - `typeName` the name of the custom type definition to generate JSON for
/// - `asset` the custom type definition to generate JSON for
/// - `supertypeName` the name of the super types for the custom type definition
const function getCustomTypeJson(typeName: String, asset: CustomAssetType, supertypeName: String): FileOutput = new FileOutput {
/// - `typeMap` full map of custom asset types
const function getCustomTypeJson(typeName: String, asset: CustomAssetType, supertypeName: String, typeMap: Mapping<String, CustomAssetType>?): FileOutput = new FileOutput {
value = new TypeDefFile {
entityDefs {
customTypeToEntityDef(asset, typeName, supertypeName)
}
enumDefs = listifyEnums(asset.enums)
structDefs = listifyStructs(asset.structs)
relationshipDefs = listifyRelationships(asset.relationships)
relationshipDefs = listifyRelationships(asset.relationships, typeMap)
}
renderer = new JsonRenderer {}
}
Expand Down Expand Up @@ -808,10 +809,19 @@ const function listifyStructs(structs: Mapping<String, StructDef>?): List<Struct
acc.add(resolveStruct(typeName, value))
)

const function listifyRelationships(relations: Mapping<String, RelationshipDef>?): List<RelationshipDef>? =
relations?.fold(List(), (acc: List<RelationshipDef>, _, value) ->
acc.add(value)
)
const function listifyRelationships(relations: Mapping<String, RelationshipDef>?, typeMap: Mapping<String, CustomAssetType>?): List<RelationshipDef>? =
new Listing {
for (_, relation: RelationshipDef in relations ?? List()) {
let (end1Attrs = typeMap?.getOrNull(relation.endDef1.type)?.attributes?.keys ?? Set())
if (end1Attrs.contains(relation.endDef1.name))
throw("Relationship \(relation.name)'s endDef1 attribute '\(relation.endDef1.name)' conflicts with an existing attribute name on type \(relation.endDef1.type).")
else
let (end2Attrs = typeMap?.getOrNull(relation.endDef2.type)?.attributes?.keys ?? Set())
if (end2Attrs.contains(relation.endDef2.name))
throw("Relationship \(relation.name)'s endDef2 attribute '\(relation.endDef2.name)' conflicts with an existing attribute name on type \(relation.endDef2.type).")
else relation
}
}.toList()

const function resolveEnum(enumName: String, enum: EnumDef): EnumDef = (enum) {
name = enumName
Expand Down Expand Up @@ -847,5 +857,5 @@ fixed customEnumDefs: List<EnumDef>? = customTypes?.fold(List(), (acc: List<Enum

/// (Generated) List of [RelationshipDef]s for the custom model.
fixed customRelationshipDefs: List<RelationshipDef>? = customTypes?.fold(List(), (acc: List<RelationshipDef>, _, details) ->
acc + (listifyRelationships(details.relationships) ?? List())
acc + (listifyRelationships(details.relationships, customTypes) ?? List())
)
19 changes: 19 additions & 0 deletions typedef-toolkit/model/src/test/kotlin/ModelUnitTest.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
/* SPDX-License-Identifier: Apache-2.0
Copyright 2023 Atlan Pte. Ltd. */
import CanonicalExampleTest.model
import com.atlan.typedef.Model
import org.pkl.config.java.ConfigEvaluator
import org.pkl.config.kotlin.forKotlin
import org.pkl.config.kotlin.to
import org.pkl.core.ModuleSource
import org.pkl.core.PklException
import org.testng.Assert.expectThrows
import org.testng.annotations.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
Expand Down Expand Up @@ -108,6 +111,22 @@ class ModelUnitTest {
assertEquals("Table", customType.superTypes[1])
}

@Test
fun conflictingAttributes() {
val exception =
expectThrows(PklException::class.java) {
evaluateModel("ConflictingAttributes")
}
assertNotNull(exception.message)
assertTrue(exception.message!!.startsWith("–– Pkl Error ––"))
val lines = exception.message!!.split("\n")
val errorMsg = lines[1]
assertEquals(
"Relationship conflicting_attributes_parent_table_conflicting_attributes_child_tables's endDef1 attribute 'conflictingAttributesChildTables' conflicts with an existing attribute name on type ConflictingAttributesTable.",
errorMsg,
)
}

private fun evaluateModel(input: String): Model {
val source = ModuleSource.path("src/test/resources/$input.pkl")
return ConfigEvaluator.preconfigured().forKotlin().use { evaluator ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
amends "modulepath:/Model.pkl"

t = "ConflictingAttributes"
a = t.decapitalize()

shared {
supertypeDefinition {
name = t
description = "Base class for all \(t) types."
superTypes { "Catalog" }
attributes {
["\(a)SourceId"] {
description = "Unique identifier for the \(t) asset from the source system."
type = "string"
}
}
}
}

customTypes {
["\(t)Table"] {
description = "Instances of \(t)Table in Atlan."
attributes {
["\(a)ChildTables"] {
description = "Simple attribute whose name will conflict with a relationship attribute."
type = "string"
}
["\(a)Ratings"] {
description = "Ratings for the \(t)Table asset from the source system."
type = "struct"
structName = "\(t)Ratings"
multiValued = true
}
}
relationships {
["\(a)_parent_child"] {
description = "Hierarchical nesting relationship between \(t)Table (parent) and \(t)Table (children)."
parent {
type = "\(t)Table"
attribute = "\(a)ChildTables"
description = "Nested tables."
}
children {
type = "\(t)Table"
attribute = "\(a)ParentTable"
description = "Parent table."
}
}
}
}
}
Loading