Skip to main content

Creating and Working with Nodes in LionWeb

LionWeb provides a flexible and language-agnostic model for working with models (or trees, or ASTs: let's consider these as synonyms in this context).

The main component is the Node.

When working with LionWeb nodes in Kotlin, there are two complementary approaches depending on your needs:

  1. Homogeneous nodes, using generic, universal APIs which work with all form of nodes. When choosing this approach, we may want to consider DynamicNode.
  2. Heterogeneous nodes, using language-specific, statically-typed Java classes, defined for a certain LionWeb language and just that one.

Regarding homogeneous nodes (a.k.a. using DynamicNode), just refer to the documentation for LionWeb Java.

For heterogeneous nodes instead, we can lever Kotlin-specific APIs.

Example: creating custom Node sub-classes in Kotlin​

You can define your own Kotlin classes by extending BaseNode and declaring features (properties, containments, references) through helper functions like property, multipleContainment, etc.

import io.lionweb.lioncore.kotlin.BaseNode
import io.lionweb.lioncore.kotlin.Implementation

interface Named {
val name: String?
}

class Tenant : BaseNode(), Named {
override var name: String? by property("name")
val users = multipleContainment<User>("users")
val directories = multipleContainment<Directory>("directories")

override fun calculateID(): String? = "tenant-${name!!}"
}

class User : BaseNode(), Named {
// Note that this means users should be unique across all tenants
override fun calculateID(): String? = "user-${name!!}"

override var name: String? by property("name")

var password: String? by property("password")
}

abstract class File : BaseNode(), Named {
override var name: String? by property("name")

override fun calculateID(): String {
val base =
if (parent == null) {
"ROOT_"
} else {
parent.id!!
}
return "${base}___${(name ?: throw IllegalStateException("Cannot calculate ID if name is not set")).replace('.', '_')}"
}

@Implementation
val path: String
get() {
return if (this.parent is File) {
"${(parent as File).path}/$name!!"
} else {
name!!
}
}
}

class Directory(id: String? = null) : File() {
init {
this.id = id
}

val files = multipleContainment<File>("files")
}

class TextFile() : File() {
var contents: String? by property("contents")

@Implementation
val numberOfLines: Int?
get() = contents?.lines()?.size
}

You can then associate node classes with corresponding concepts, if they have been created programmatically or loaded from an existing language:

registerMapping(Tenant::class, TenantConcept)
registerMapping(User::class, UserConcept)
registerMapping(File::class, FileConcept)
registerMapping(Directory::class, DirectoryConcept)
registerMapping(TextFile::class, TextFileConcept)

This registers the mapping in the MetamodelRegistry, so LionWeb can match Kotlin classes to their concepts when serializing or deserializing nodes.

Here's a full example that:

  • Creates a language dynamically
  • Registers concept mappings
  • Builds a model
  • Serializes and deserializes it
  • Automatically reuses the Kotlin node classes
import io.lionweb.language.Concept
import io.lionweb.language.LionCoreBuiltins
import io.lionweb.model.impl.DynamicNode
import io.lionweb.serialization.SerializationProvider
import io.lionweb.kotlin.MetamodelRegistry
import io.lionweb.kotlin.MetamodelRegistry.registerMapping
import io.lionweb.kotlin.Multiplicity
import io.lionweb.kotlin.createConcept
import io.lionweb.kotlin.createContainment
import io.lionweb.kotlin.createProperty
import io.lionweb.kotlin.lwLanguage
import kotlin.test.assertTrue

fun main(args: Array<String>) {
val tenantConcept: Concept
val userConcept: Concept
val fileConcept: Concept
val directoryConcept: Concept
val textFileConcept: Concept
val organizationLanguage =
lwLanguage("Organization").apply {
tenantConcept = createConcept("Tenant").apply {
isPartition = true
addImplementedInterface(LionCoreBuiltins.getINamed())
}
userConcept = createConcept("User").apply {
addImplementedInterface(LionCoreBuiltins.getINamed())
createProperty("password", LionCoreBuiltins.getString())
}
fileConcept = createConcept("File").apply {
isAbstract = true
addImplementedInterface(LionCoreBuiltins.getINamed())
}
directoryConcept = createConcept("Directory").apply {
extendedConcept = fileConcept
}
textFileConcept = createConcept("TextFile").apply {
extendedConcept = fileConcept
createProperty("contents", LionCoreBuiltins.getString())
}

tenantConcept.createContainment("users", userConcept, Multiplicity.ZERO_TO_MANY)
tenantConcept.createContainment("directories", directoryConcept, Multiplicity.ZERO_TO_MANY)

directoryConcept.createContainment("files", fileConcept, Multiplicity.ZERO_TO_MANY)
}

registerMapping(Tenant::class, tenantConcept)
registerMapping(User::class, userConcept)
registerMapping(File::class, fileConcept)
registerMapping(Directory::class, directoryConcept)
registerMapping(TextFile::class, textFileConcept)

val tenant1 = Tenant().apply {
name = "My Tenant"
users.add(User().apply {
name = "Gino"
password = "FerraraBiciclette87"
})
directories.add(Directory().apply {
name = "root"
files.add(TextFile().apply {
name = "foo.json"
contents = "{}"
})
})
}

val jsonSerialization = SerializationProvider.getStandardJsonSerialization()
val tenant1Serialized = jsonSerialization.serializeTreeToJsonString(tenant1)

jsonSerialization.enableDynamicNodes()
var deserializedTenant1 = jsonSerialization.deserializeToNodes(tenant1Serialized).first()
assertTrue(deserializedTenant1 is DynamicNode)

MetamodelRegistry.prepareJsonSerialization(jsonSerialization)

deserializedTenant1 = jsonSerialization.deserializeToNodes(tenant1Serialized).first()
assertTrue(deserializedTenant1 is Tenant)
}

Automatically Deriving Languages from Kotlin Classes​

Instead of manually defining the language, you can derive the entire language structure from your node subclasses:

val organizationLanguage = lwLanguage(
"Organization",
Tenant::class, User::class, File::class,
Directory::class, TextFile::class
)

LionWeb will:

  • Inspect the properties and containments declared with property, multipleContainment, etc.
  • Derive the corresponding LionWeb concepts and features
  • Automatically register mappings

This is ideal for quick prototyping and avoids duplication.

Here there is the complete example:

import io.lionweb.model.impl.DynamicNode
import io.lionweb.serialization.SerializationProvider
import io.lionweb.kotlin.MetamodelRegistry
import io.lionweb.kotlin.lwLanguage
import kotlin.test.assertTrue

fun main(args: Array<String>) {
val organizationLanguage =
lwLanguage("Organization", Tenant::class, User::class, File::class,
Directory::class, TextFile::class)
val tenantConcept = organizationLanguage.getConceptByName("Tenant")!!
val userConcept = organizationLanguage.getConceptByName("User")!!
val fileConcept = organizationLanguage.getConceptByName("File")!!
val directoryConcept = organizationLanguage.getConceptByName("Directory")!!
val textFileConcept = organizationLanguage.getConceptByName("TextFile")!!

val tenant1 = Tenant().apply {
name = "My Tenant"
users.add(User().apply {
name = "Gino"
password = "FerraraBiciclette87"
})
directories.add(Directory().apply {
name = "root"
files.add(TextFile().apply {
name = "foo.json"
contents = "{}"
})
})
}

val jsonSerialization = SerializationProvider.getStandardJsonSerialization()
val tenant1Serialized = jsonSerialization.serializeTreeToJsonString(tenant1)

jsonSerialization.enableDynamicNodes()
var deserializedTenant1 = jsonSerialization.deserializeToNodes(tenant1Serialized).first()
assertTrue(deserializedTenant1 is DynamicNode)

MetamodelRegistry.prepareJsonSerialization(jsonSerialization)

deserializedTenant1 = jsonSerialization.deserializeToNodes(tenant1Serialized).first()
assertTrue(deserializedTenant1 is Tenant)
}

Key Features and Mechanisms​

FeatureDescription
BaseNodeSuperclass for all custom nodes
property(...)Declares a LionWeb property tied to a Kotlin property
multipleContainment(...)Declares a multi-valued containment reference
calculateID()Override to auto-assign node IDs
@ImplementationMarks a Kotlin property as implementation-only, not part of the metamodel
registerMapping(...)Binds a Kotlin class to a LionWeb concept
lwLanguage("Name", classes...)Derives a full language from node classes

Summary​

You have two options when working with node classes in Kotlin:

  • Manual mapping:

    • Define your language programmatically
    • Register mappings with registerMapping
    • Use full control over the language definition
  • Auto-derive from node classes:

    • Write node subclasses using BaseNode and helpers
    • Use lwLanguage(...) to derive the metamodel

This dual approach lets you choose the right balance between flexibility and convenience.