Best Practices

This guide provides best practices for using Kastor Gen effectively in your projects.

Table of Contents

Domain Interface Design

✅ Keep Interfaces Pure

Design domain interfaces that focus on business logic without RDF dependencies:

// ✅ Good: Pure domain interface
@Rdf(iri = "http://www.w3.org/ns/dcat#Catalog")
interface Catalog {
    @Rdf(iri = "http://purl.org/dc/terms/title")
    val title: String
    
    @Rdf(iri = "http://www.w3.org/ns/dcat#dataset")
    val dataset: List<Dataset>
}

// ❌ Avoid: RDF types in domain interface
interface Catalog {
    val title: String
    val dataset: List<Dataset>
    val rdfNode: RdfTerm // Don't expose RDF types
}

✅ Use Descriptive Property Names

Choose property names that reflect business concepts:

// ✅ Good: Business-focused names
interface Person {
    @Rdf(iri = "http://xmlns.com/foaf/0.1/name")
    val fullName: String
    
    @Rdf(iri = "http://xmlns.com/foaf/0.1/knows")
    val acquaintances: List<Person>
}

// ❌ Avoid: Technical RDF names
interface Person {
    @Rdf(iri = "http://xmlns.com/foaf/0.1/name")
    val foafName: String
    
    @Rdf(iri = "http://xmlns.com/foaf/0.1/knows")
    val foafKnows: List<Person>
}

✅ Handle Cardinality Appropriately

Design properties based on expected cardinality:

// ✅ Good: Appropriate cardinality
interface Person {
    @Rdf(iri = "http://xmlns.com/foaf/0.1/name")
    val name: String // Single value expected
    
    @Rdf(iri = "http://xmlns.com/foaf/0.1/knows")
    val knows: List<Person> // Multiple values expected
}

// ❌ Avoid: Inappropriate cardinality
interface Person {
    @Rdf(iri = "http://xmlns.com/foaf/0.1/name")
    val name: List<String> // Overkill for single name
    
    @Rdf(iri = "http://xmlns.com/foaf/0.1/knows")
    val knows: Person // Should be List<Person>
}

Ontology Management

✅ Use SHACL for Validation

Define SHACL shapes for your ontologies:

@prefix dcat: <http://www.w3.org/ns/dcat#> .
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

<http://example.org/shapes/Catalog>
    a sh:NodeShape ;
    sh:targetClass dcat:Catalog ;
    sh:property [
        sh:path dcterms:title ;
        sh:name "title" ;
        sh:description "A name given to the catalog." ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
    ] .

✅ Generate Code from Ontologies

Use ontology-driven code generation for consistency:

import com.geoknoesis.kastor.gen.annotations.Rdf

@Rdf(
    shacl = "ontologies/dcat.shacl.ttl",
    context = "ontologies/dcat.context.jsonld",
    packageName = "com.example.generated",
    generateInterfaces = true,
    generateWrappers = true,
)
class OntologyGenerator

✅ Version Your Ontologies

Track ontology changes in version control:

src/main/resources/ontologies/
├── dcat-v1.0.shacl.ttl
├── dcat-v1.0.context.jsonld
├── dcat-v2.0.shacl.ttl
└── dcat-v2.0.context.jsonld

Performance Optimization

✅ Use Lazy Evaluation

Properties are evaluated lazily by default:

// ✅ Good: Lazy evaluation (automatic)
val catalog: Catalog = catalogRef.asType()
val title = catalog.title // Only evaluated when accessed

// ❌ Avoid: Eager evaluation
val catalog: Catalog = catalogRef.asType()
val allProperties = listOf(
    catalog.title,
    catalog.description,
    catalog.dataset
) // All evaluated immediately

✅ Minimize RDF Graph Traversal

Access related objects efficiently:

// ✅ Good: Efficient access
val catalog: Catalog = catalogRef.asType()
val firstDataset = catalog.dataset.firstOrNull()
val firstDistribution = firstDataset?.distribution?.firstOrNull()

// ❌ Avoid: Multiple graph traversals
val catalog: Catalog = catalogRef.asType()
val datasets = catalog.dataset
val distributions = datasets.flatMap { it.distribution }
val titles = distributions.map { it.title }

✅ Use Side-Channel for Bulk Operations

Access RDF directly for bulk operations:

// ✅ Good: Bulk access via side-channel
val rdfHandle = catalog.asRdf()
val extras = rdfHandle.extras
val allPredicates = extras.predicates()
val allValues = allPredicates.flatMap { extras.values(it) }

// ❌ Avoid: Individual property access
val catalog: Catalog = catalogRef.asType()
val title = catalog.title
val description = catalog.description
val dataset = catalog.dataset

Error Handling

✅ Handle Missing Properties Gracefully

Design interfaces to handle optional properties:

// ✅ Good: Optional properties
interface Person {
    @Rdf(iri = "http://xmlns.com/foaf/0.1/name")
    val name: String // Required
    
    @Rdf(iri = "http://xmlns.com/foaf/0.1/age")
    val age: Int? // Optional
}

// Usage
val person: Person = personRef.asType()
val ageText = person.age?.toString() ?: "Unknown"

✅ Validate Data When Needed

Use validation for critical operations:

// ✅ Good: Validation for critical operations
val validation = JenaValidation.fromTurtle(shapesTtl) // shapesTtl: your SHACL shapes as Turtle text
val catalog: Catalog = catalogRef.asValidatedType(validation)

// Or validate explicitly
val rdfHandle = catalog.asRdf()
try {
    rdfHandle.validateOrThrow()
    println("Validation passed")
} catch (e: ValidationException) {
    println("Validation failed: ${e.message}")
}

✅ Handle Type Conversion Errors

Be prepared for type conversion failures:

// ✅ Good: Safe type conversion
val wrapper = object : RdfBacked {
    override val rdf = handle
    override val age: Int by lazy {
        try {
            KastorGraphOps.getLiteralValues(rdf.graph, rdf.node, FOAF.age)
                .map { it.lexical.toInt() }.firstOrNull() ?: 0
        } catch (e: NumberFormatException) {
            0 // Default value
        }
    }
}

Testing Strategies

✅ Test Domain Interfaces Independently

Test business logic without RDF dependencies:

// ✅ Good: Test domain logic
class CatalogBusinessLogicTest {
    @Test
    fun `catalog should have valid title`() {
        val catalog = mockk<Catalog>()
        every { catalog.title } returns "Test Catalog"
        
        assertThat(catalog.title).isEqualTo("Test Catalog")
    }
}

✅ Test RDF Integration Separately

Test RDF materialization and side-channel access:

// ✅ Good: Test RDF integration
class CatalogRdfIntegrationTest {
    @Test
    fun `catalog materialization works`() {
        val repo = Rdf.memory()
        repo.add {
            val catalog = iri("http://example.org/catalog")
            catalog - RDF.type - DCAT.Catalog
            catalog - DCTERMS.title - "Test Catalog"
        }
        
        val catalogRef = RdfRef(iri("http://example.org/catalog"), repo.defaultGraph)
        val catalog: Catalog = catalogRef.asType()
        
        assertThat(catalog.title).isEqualTo("Test Catalog")
    }
}

✅ Test Generated Code

Verify generated interfaces and wrappers:

// ✅ Good: Test generated code
class GeneratedCatalogTest {
    @Test
    fun `generated catalog interface has expected properties`() {
        val catalogClass = Catalog::class.java
        val methods = catalogClass.declaredMethods
        
        assertThat(methods).anyMatch { it.name == "getTitle" }
        assertThat(methods).anyMatch { it.name == "getDataset" }
    }
}

Code Organization

✅ Separate Generated Code

Keep generated code separate from hand-written code:

src/main/kotlin/
├── com/example/
│   ├── domain/           # Hand-written domain interfaces
│   ├── generated/        # Generated interfaces and wrappers
│   ├── business/         # Business logic
│   └── integration/      # RDF integration

✅ Use Package Structure

Organize code by domain and functionality:

// ✅ Good: Organized packages
package com.example.dcatus.domain
interface Catalog { ... }

package com.example.dcatus.generated
class CatalogWrapper { ... }

package com.example.dcatus.business
class CatalogService { ... }

package com.example.dcatus.integration
class CatalogRepository { ... }

✅ Configuration Management

Centralize configuration for different environments:

// ✅ Good: Configuration management
object OntoMapperConfig {
    val validationEnabled: Boolean = System.getProperty("validation.enabled", "false").toBoolean()
    val shaclShapesPath: String = System.getProperty("shacl.shapes.path", "ontologies/shapes.ttl")
    
    fun initialize() {
        if (validationEnabled) {
            val validation = JenaValidation.fromTurtle(shapesTtl) // shapesTtl: your SHACL shapes as Turtle text
        }
    }
}

Security Considerations

✅ Validate Input Data

Always validate RDF data before processing:

// ✅ Good: Input validation
fun loadCatalog(iri: String, graph: RdfGraph): Catalog {
    require(iri.isNotBlank()) { "IRI cannot be blank" }
    require(graph.getTriples().isNotEmpty()) { "Graph cannot be empty" }
    
    val validation = JenaValidation.fromTurtle(shapesTtl) // shapesTtl: your SHACL shapes as Turtle text
    val catalogRef = RdfRef(Iri(iri), graph)
    return catalogRef.asValidatedType(validation)
}

✅ Handle Sensitive Data

Be careful with sensitive information in RDF:

// ✅ Good: Sensitive data handling
interface Person {
    @Rdf(iri = "http://xmlns.com/foaf/0.1/name")
    val name: String
    
    // Don't expose sensitive data through domain interface
    // Handle separately with proper security controls
}

✅ Use Secure RDF Sources

Validate RDF sources and use secure connections:

// ✅ Good: Secure RDF source
fun loadSecureCatalog(url: String): Catalog {
    require(url.startsWith("https://")) { "Only HTTPS URLs allowed" }
    
    val repo = RdfProviderRegistry.create(
        RdfConfig.of(
            providerId = ProviderId("sparql"),
            variantId = VariantId("sparql"),
            options = mapOf("location" to url)
        )
    )
    val graph = repo.defaultGraph
    return loadCatalog(url, graph)
}

Deployment Guidelines

✅ Build Configuration

Configure build for different environments:

// build.gradle.kts
plugins {
    id("org.jetbrains.kotlin.jvm")
    id("com.google.devtools.ksp")
}

dependencies {
    implementation(project(":kastor-gen:runtime"))
    ksp(project(":kastor-gen:processor"))
}

ksp {
    // The only processor option: extra directories searched for @Rdf(shacl/context/ontologyPath) files
    arg("kastor.gen.resources", "${projectDir}/ontologies")
}

✅ Runtime Configuration

Configure runtime behavior:

import com.geoknoesis.kastor.gen.runtime.OntoMapper
import com.geoknoesis.kastor.gen.validation.jena.JenaValidation
import org.slf4j.LoggerFactory

// ✅ Good: Runtime configuration
fun initializeOntoMapper() {
    val validation = JenaValidation.fromTurtle(shapesTtl) // optional module `kastor-gen:validation-jena`; shapesTtl = SHACL Turtle

    // Eager-load wrapper classes you materialize in hot paths
    OntoMapper.initialize(Person::class.java, Catalog::class.java)

    LoggerFactory.getLogger(OntoMapper::class.java).info("OntoMapper ready ({} factories)", OntoMapper.registeredTypes().size)
}

✅ Monitoring and Logging

Add monitoring for production deployments:

// ✅ Good: Monitoring
class OntoMapperMetrics {
    private val materializationCounter = Counter.build()
        .name("kastor_gen_materializations_total")
        .help("Total number of materializations")
        .register()
    
    fun <T> materializeWithMetrics(ref: RdfRef, type: Class<T>): T {
        materializationCounter.inc()
        return OntoMapper.materialize(ref, type)
    }
}

Common Pitfalls

❌ Avoid Exposing RDF Types

Don’t expose RDF types in domain interfaces:

// ❌ Bad: RDF types in domain interface
interface Catalog {
    val title: String
    val rdfNode: RdfTerm // Don't do this
    val graph: RdfGraph // Don't do this
}

❌ Avoid Eager Evaluation

Don’t force eager evaluation of properties:

// ❌ Bad: Eager evaluation
val catalog: Catalog = catalogRef.asType()
val allData = listOf(
    catalog.title,
    catalog.description,
    catalog.dataset
) // All properties evaluated immediately

❌ Avoid Complex Inheritance

Keep inheritance simple and focused:

// ❌ Bad: Complex inheritance
interface Resource {
    val iri: String
}

interface Catalog : Resource {
    val title: String
}

interface Dataset : Resource {
    val title: String
}

interface CatalogDataset : Catalog, Dataset {
    // Complex inheritance can cause issues
}

❌ Avoid Mutable State

Keep domain interfaces immutable:

// ❌ Bad: Mutable state
interface Catalog {
    var title: String // Don't use var
    val dataset: MutableList<Dataset> // Don't use mutable collections
}

Conclusion

Following these best practices will help you build robust, maintainable applications with Kastor Gen. Remember to:

  1. Keep domain interfaces pure - Focus on business logic
  2. Use ontology-driven generation - Maintain consistency
  3. Optimize for performance - Use lazy evaluation and bulk operations
  4. Handle errors gracefully - Validate data and handle edge cases
  5. Test thoroughly - Test both domain logic and RDF integration
  6. Organize code well - Separate concerns and use clear package structure
  7. Consider security - Validate input and handle sensitive data
  8. Configure for deployment - Set up proper build and runtime configuration

For more specific guidance, see the FAQ and API Reference.