Best Practices
This guide provides best practices for using Kastor Gen effectively in your projects.
Table of Contents
- Domain Interface Design
- Ontology Management
- Performance Optimization
- Error Handling
- Testing Strategies
- Code Organization
- Security Considerations
- Deployment Guidelines
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:
- Keep domain interfaces pure - Focus on business logic
- Use ontology-driven generation - Maintain consistency
- Optimize for performance - Use lazy evaluation and bulk operations
- Handle errors gracefully - Validate data and handle edge cases
- Test thoroughly - Test both domain logic and RDF integration
- Organize code well - Separate concerns and use clear package structure
- Consider security - Validate input and handle sensitive data
- Configure for deployment - Set up proper build and runtime configuration
For more specific guidance, see the FAQ and API Reference.