Getting Started
To start using the validation DSL, you first need to create a validator:Copy
import xyz.mahmoudahmed.dsl.validation.*
// Create a pre-validation validator
val preValidator = ValidationDsl.preValidate {
// Validation rules go here
}
// Create an in-validation validator
val inValidator = ValidationDsl.inValidate {
// Validation rules go here
}
// Create a post-validation validator
val postValidator = ValidationDsl.postValidate {
// Validation rules go here
}
Basic Field Validation
The most common validation is checking individual fields:Copy
val userValidator = ValidationDsl.preValidate {
// Required field (must not be null)
validate("username")
.required()
.end()
// String length validation
validate("name")
.required()
.minLength(2)
.maxLength(100)
.end()
// Pattern matching with regex
validate("zipCode")
.pattern(Regex("^\\d{5}(-\\d{4})?$"))
.end()
// Numeric value validation
validate("age")
.min(18)
.max(120)
.end()
// Email validation
validate("email")
.required()
.email()
.end()
// Date validation
validate("birthDate")
.date("yyyy-MM-dd")
.end()
}
Complex Validation Rules
You can create more complex validation rules:Copy
val orderValidator = ValidationDsl.preValidate {
// Custom validation with a predicate
validate("orderCode")
.custom(
predicate = { it?.toString()?.startsWith("ORD-") == true },
message = "Order code must start with 'ORD-'"
)
.end()
// Allowed values (enum-like validation)
validate("status")
.allowedValues("NEW", "PROCESSING", "SHIPPED", "DELIVERED", "CANCELLED")
.end()
// Validate related fields as a group
validateGroup(
"subtotal",
"tax",
"total",
message = "Total must equal subtotal plus tax",
code = "TOTAL_MISMATCH"
) { values ->
val subtotal = (values[0] as? Number)?.toDouble() ?: 0.0
val tax = (values[1] as? Number)?.toDouble() ?: 0.0
val total = (values[2] as? Number)?.toDouble() ?: 0.0
// Allow small rounding differences
Math.abs((subtotal + tax) - total) < 0.01
}
// Field required only when another field has specific value
validate("trackingNumber")
.requiredIf("status", "SHIPPED", "DELIVERED")
.end()
}
Conditional Validation
Apply validation rules only when certain conditions are met:Copy
val profileValidator = ValidationDsl.preValidate {
// Basic validation for everyone
validate("name").required().end()
validate("email").required().email().end()
// Validate age only for specific countries
validate("age")
.required()
.min(18)
.`when` { data ->
val country = (data as? Map<*, *>)?.get("country")?.toString()
country == "US" || country == "CA"
}
.end()
// Conditional block of validations
`when`(condition = { data ->
(data as? Map<*, *>)?.get("accountType")?.toString() == "BUSINESS"
}) {
validate("businessName").required().end()
validate("taxId").required().pattern(Regex("^\\d{2}-\\d{7}$")).end()
validate("industry").required().end()
}
// Validation with different severity level
validate("phoneNumber")
.pattern(Regex("^\\+?[0-9]{10,15}$"))
.asWarning() // This won't cause validation to fail
.end()
}
Collection Validation
Validate collections and their items:Copy
val orderValidator = ValidationDsl.preValidate {
// Validate the collection itself
validate("items")
.required()
.size(min = 1, max = 100)
.end()
// Validate each item in a collection
forEach("items") {
validate("productId")
.required()
.pattern(Regex("^PROD-\\d+$"))
.end()
validate("quantity")
.required()
.min(1)
.end()
validate("price")
.required()
.min(0.01)
.end()
}
// Validate all fields matching a pattern
validateAll("customer.preferences.*")
.required()
.end()
}
Validation Phases
PlatyMap supports three validation phases:-
Pre-Validation: Validates source data before mapping
Copy
val preValidator = ValidationDsl.preValidate { // Validate source data } -
In-Validation: Validates during the mapping process
Copy
val mapping = Platymap.flow("source") .to("target") .validateDuring { // Validate during mapping } .build() -
Post-Validation: Validates target data after mapping
Copy
val postValidator = ValidationDsl.postValidate { // Validate target data }
Integration with Mapping
You can integrate validation with mapping in two ways:Method 1: Separate Validation and Mapping
Copy
// Define validators
val preValidator = ValidationDsl.preValidate { /* validation rules */ }
val postValidator = ValidationDsl.postValidate { /* validation rules */ }
// Define mapping
val mapping = Platymap.flow("source")
.to("target")
.map("source.name").to("target.fullName")
// More mapping rules...
.build()
// Create validated mapping
val validatedMapping = createValidatedMapping(
mapping,
preValidator = preValidator,
postValidator = postValidator,
config = ValidationConfig(
failFast = false,
throwOnError = true,
includeWarnings = true
)
)
// Execute the validated mapping
try {
val result = validatedMapping.executeToJson(sourceData)
println("Mapping successful")
} catch (e: ValidationException) {
println("Validation failed: ${e.message}")
e.errors.forEach { error ->
println("- ${error.path}: ${error.message}")
}
}
Method 2: Fluent API Integration
Copy
// Define mapping with integrated validation
val mapping = Platymap.flow("source")
.to("target")
// Add pre-validation
.withPreValidation {
validate("source.id").required().end()
validate("source.email").email().end()
}
// Define mapping rules
.map("source.id").to("target.id")
.map("source.email").to("target.contactEmail")
// Add in-validation
.validateDuring {
validate("source.data").required().end()
}
// More mapping rules
.map("source.data").to("target.info")
// Add post-validation
.withPostValidation {
validate("target.id").required().end()
validate("target.contactEmail").required().email().end()
}
// Build with validation
.buildWithValidation()
// Execute the mapping
try {
val result = mapping.executeToJson(sourceData)
println("Mapping successful")
} catch (e: ValidationException) {
println("Validation failed")
}
Error Handling
Validation producesValidationResult objects that contain information about validation failures:
Copy
val validator = ValidationDsl.preValidate {
// Validation rules...
}
// Validate data
val result = validator.validate(data)
if (result.isValid) {
println("Validation passed!")
} else {
println("Validation failed with ${result.errors.size} errors:")
result.errors.forEach { error ->
println("Field: ${error.path}")
println("Message: ${error.message}")
println("Value: ${error.value}")
println("Code: ${error.code}")
println("Severity: ${error.severity}")
println()
}
}
ValidationConfig:
Copy
val config = ValidationConfig(
failFast = true, // Stop at first error
throwOnError = false, // Don't throw exceptions
includeWarnings = true, // Include warnings in result
includeInfos = false // Exclude info messages
)
val result = validator.validate(data, config)
Best Practices
- Organize validators logically: Group related validations together
- Be specific with error messages: Provide clear guidance on how to fix issues
- Use appropriate severity levels: Not all issues are errors
- Validate at the right phase: Use pre-validation for input data, post-validation for output data
- Reuse validation logic: Extract common validation patterns to functions
Complete Examples
Customer Registration Validation
Copy
val registrationValidator = ValidationDsl.preValidate {
// Personal information
validate("customer.firstName")
.required()
.minLength(2)
.maxLength(50)
.end()
validate("customer.lastName")
.required()
.minLength(2)
.maxLength(50)
.end()
validate("customer.email")
.required()
.email()
.end()
validate("customer.password")
.required()
.minLength(8)
.custom(
predicate = { password ->
// Password must contain at least one uppercase letter, one lowercase letter,
// one digit, and one special character
val passwordStr = password.toString()
passwordStr.any { it.isUpperCase() } &&
passwordStr.any { it.isLowerCase() } &&
passwordStr.any { it.isDigit() } &&
passwordStr.any { !it.isLetterOrDigit() }
},
message = "Password must include uppercase, lowercase, digit, and special character"
)
.end()
// Address validation
validate("customer.address.street")
.required()
.minLength(5)
.end()
validate("customer.address.city")
.required()
.end()
validate("customer.address.state")
.required()
.pattern(Regex("^[A-Z]{2}$"))
.end()
validate("customer.address.zipCode")
.required()
.pattern(Regex("^\\d{5}(-\\d{4})?$"))
.end()
// Payment information
`when`(condition = { data ->
(data as? Map<*, *>)?.get("paymentMethod")?.toString() == "CREDIT_CARD"
}) {
validate("paymentInfo.cardNumber")
.required()
.pattern(Regex("^\\d{16}$"))
.end()
validate("paymentInfo.expiryDate")
.required()
.pattern(Regex("^(0[1-9]|1[0-2])/\\d{2}$"))
.end()
validate("paymentInfo.cvv")
.required()
.pattern(Regex("^\\d{3,4}$"))
.end()
}
// Marketing preferences
validate("preferences.receiveNewsletter")
.custom(
predicate = { it is Boolean },
message = "Receive newsletter preference must be a boolean"
)
.asWarning()
.end()
}
// Sample usage
val customerData = mapOf(
"customer" to mapOf(
"firstName" to "John",
"lastName" to "Doe",
"email" to "[email protected]",
"password" to "P@ssw0rd",
"address" to mapOf(
"street" to "123 Main St",
"city" to "Anytown",
"state" to "NY",
"zipCode" to "12345"
)
),
"paymentMethod" to "CREDIT_CARD",
"paymentInfo" to mapOf(
"cardNumber" to "4111111111111111",
"expiryDate" to "12/25",
"cvv" to "123"
),
"preferences" to mapOf(
"receiveNewsletter" to true
)
)
val result = registrationValidator.validate(customerData)
if (result.isValid) {
// Process the registration
println("Registration is valid")
} else {
// Handle validation errors
println("Registration has errors:")
result.errors.forEach { error ->
println("- ${error.path}: ${error.message}")
}
}
Order Processing with Validation and Mapping
Copy
// Create a complete order processing pipeline with validation
val orderProcessing = Platymap.flow("order")
.withFormat(Format.JSON)
.to("processedOrder")
// Pre-validate the order
.withPreValidation {
// Basic order validation
validate("order.id")
.required()
.pattern(Regex("^ORD-\\d+$"))
.end()
validate("order.customer.id")
.required()
.end()
validate("order.date")
.required()
.date("yyyy-MM-dd'T'HH:mm:ss'Z'")
.end()
// Items validation
validate("order.items")
.required()
.size(min = 1)
.end()
forEach("order.items") {
validate("productId")
.required()
.end()
validate("quantity")
.required()
.min(1)
.end()
validate("price")
.required()
.min(0.01)
.end()
}
// Validate order total
validateGroup(
"order.subtotal",
"order.tax",
"order.total",
message = "Order total must equal subtotal plus tax"
) { values ->
val subtotal = (values[0] as? Number)?.toDouble() ?: 0.0
val tax = (values[1] as? Number)?.toDouble() ?: 0.0
val total = (values[2] as? Number)?.toDouble() ?: 0.0
Math.abs((subtotal + tax) - total) < 0.01
}
}
// Mapping logic
.map("order.id").to("orderId")
.map("order.date").to("orderDate")
.map("order.customer.id").to("customerId")
.map("order.customer.name").to("customerName")
// Map items with validation during mapping
.forEach("order.items")
.`as`("item")
.create("items")
.map("$item.productId").to("id")
.map("$item.name").to("description")
.map("$item.price").to("unitPrice")
.map("$item.quantity").to("quantity")
.map("$item.price").transform { price ->
val quantity = context.getValueByPath("$item.quantity") as? Number ?: 1
val priceValue = (price as? Number)?.toDouble() ?: 0.0
priceValue * quantity.toDouble()
}.to("totalPrice")
.end()
.end()
// Additional validation during mapping
.validateDuring {
forEach("order.items") {
```kotlin
validateGroup(
"$item.price",
"$item.quantity",
"$item.totalPrice",
message = "Item totalPrice must equal price * quantity"
) { values ->
val price = (values[0] as? Number)?.toDouble() ?: 0.0
val quantity = (values[1] as? Number)?.toInt() ?: 0
val totalPrice = (values[2] as? Number)?.toDouble() ?: 0.0
Math.abs((price * quantity) - totalPrice) < 0.01
}
}
}
// Map totals
.map("order.subtotal").to("totals.subtotal")
.map("order.tax").to("totals.tax")
.map("order.total").to("totals.total")
// Add shipping information with conditional logic
.branch()
.when { data ->
val shippingMethod = (data as? Map<*, *>)?.get("order")?.let {
(it as? Map<*, *>)?.get("shipping")?.let { shipping ->
(shipping as? Map<*, *>)?.get("method")?.toString()
}
}
shippingMethod == "express" || shippingMethod == "overnight"
}
.then()
.map("order.shipping.trackingNumber").to("shipping.trackingCode")
.map("shippingPriority").transform { "HIGH" }.to("shipping.priority")
.endBranch()
.otherwise()
.map("order.shipping.trackingNumber").to("shipping.trackingCode")
.map("shippingPriority").transform { "NORMAL" }.to("shipping.priority")
.endBranch()
.end()
// Post-validate the processed order
.withPostValidation {
// Ensure required fields exist in output
validate("orderId").required().end()
validate("orderDate").required().end()
validate("customerId").required().end()
// Validate items were mapped correctly
validate("items").required().size(min = 1).end()
forEach("items") {
validate("id").required().end()
validate("unitPrice").required().min(0).end()
validate("quantity").required().min(1).end()
validate("totalPrice").required().min(0).end()
// Verify calculations are correct
validateGroup(
"unitPrice",
"quantity",
"totalPrice",
message = "Item total price calculation is incorrect"
) { values ->
val unitPrice = (values[0] as? Number)?.toDouble() ?: 0.0
val quantity = (values[1] as? Number)?.toInt() ?: 0
val totalPrice = (values[2] as? Number)?.toDouble() ?: 0.0
Math.abs((unitPrice * quantity) - totalPrice) < 0.01
}
}
// Validate totals
validate("totals.subtotal").required().min(0).end()
validate("totals.tax").required().min(0).end()
validate("totals.total").required().min(0).end()
// Verify total calculation
validateGroup(
"totals.subtotal",
"totals.tax",
"totals.total",
message = "Order total calculation is incorrect"
) { values ->
val subtotal = (values[0] as? Number)?.toDouble() ?: 0.0
val tax = (values[1] as? Number)?.toDouble() ?: 0.0
val total = (values[2] as? Number)?.toDouble() ?: 0.0
Math.abs((subtotal + tax) - total) < 0.01
}
}
// Build with validation
.buildWithValidation(
ValidationConfig(
failFast = false,
throwOnError = true,
includeWarnings = true
)
)
// Sample order data
val orderJson = """
{
"order": {
"id": "ORD-12345",
"date": "2023-09-15T10:30:00Z",
"customer": {
"id": "CUST-789",
"name": "Jane Smith",
"email": "[email protected]"
},
"items": [
{
"productId": "PROD-001",
"name": "Smartphone",
"price": 699.99,
"quantity": 1,
"totalPrice": 699.99
},
{
"productId": "PROD-002",
"name": "Phone Case",
"price": 24.99,
"quantity": 2,
"totalPrice": 49.98
}
],
"shipping": {
"method": "express",
"trackingNumber": "TRK987654321",
"estimatedDelivery": "2023-09-18"
},
"subtotal": 749.97,
"tax": 60.00,
"total": 809.97
}
}
"""
// Execute the validated mapping
try {
val result = orderProcessing.executeToJson(orderJson)
println("Order processing successful!")
println(result)
} catch (e: ValidationException) {
println("Order validation failed:")
e.errors.forEach { error ->
println("- ${error.path}: ${error.message} (${error.severity})")
}
}
Common Validation Scenarios
Here are examples of common validation scenarios and how to handle them:1. Form Data Validation
Copy
val formValidator = ValidationDsl.preValidate {
// Text fields
validate("formData.name")
.required()
.minLength(2)
.maxLength(100)
.end()
validate("formData.email")
.required()
.email()
.end()
// Phone with optional format
validate("formData.phone")
.pattern(Regex("^\\+?[0-9\\s-()]{10,20}$"))
.asWarning() // Only warn for invalid format
.end()
// Selection field
validate("formData.category")
.required()
.allowedValues("personal", "business", "education", "other")
.end()
// Number field
validate("formData.age")
.min(18)
.max(120)
.end()
// Checkbox confirmation
validate("formData.termsAccepted")
.custom(
predicate = { it == true },
message = "You must accept the terms and conditions"
)
.end()
// Date field
validate("formData.eventDate")
.date("yyyy-MM-dd")
.custom(
predicate = { date ->
// Must be a future date
if (date == null) return@custom true
val parsedDate = java.time.LocalDate.parse(
date.toString(),
java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd")
)
parsedDate.isAfter(java.time.LocalDate.now())
},
message = "Event date must be in the future"
)
.end()
}
2. API Response Validation
Copy
val apiResponseValidator = ValidationDsl.postValidate {
// Check API status
validate("status")
.required()
.allowedValues("success", "error", "pending")
.end()
// Conditional validation based on status
`when`(condition = { data ->
(data as? Map<*, *>)?.get("status") == "success"
}) {
validate("data")
.required()
.end()
// Check pagination if present
`when`(condition = { data ->
(data as? Map<*, *>)?.get("data")?.let {
(it as? Map<*, *>)?.containsKey("pagination")
} == true
}) {
validate("data.pagination.total")
.required()
.min(0)
.end()
validate("data.pagination.page")
.required()
.min(1)
.end()
validate("data.pagination.perPage")
.required()
.min(1)
.end()
// Verify pagination math
validateGroup(
"data.pagination.total",
"data.pagination.perPage",
"data.pagination.totalPages",
message = "Pagination calculation is incorrect"
) { values ->
val total = (values[0] as? Number)?.toInt() ?: 0
val perPage = (values[1] as? Number)?.toInt() ?: 1
val totalPages = (values[2] as? Number)?.toInt() ?: 0
val expectedTotalPages = Math.ceil(total.toDouble() / perPage).toInt()
expectedTotalPages == totalPages
}
}
}
// Error response validation
`when`(condition = { data ->
(data as? Map<*, *>)?.get("status") == "error"
}) {
validate("error")
.required()
.end()
validate("error.code")
.required()
.end()
validate("error.message")
.required()
.end()
}
}
3. Configuration Validation
Copy
val configValidator = ValidationDsl.preValidate {
// Database configuration
validate("config.database.url")
.required()
.pattern(Regex("^jdbc:[a-zA-Z0-9:]+://[^\\s/$.?#]+\\.[^\\s]+$"))
.end()
validate("config.database.username")
.required()
.end()
validate("config.database.password")
.required()
.minLength(8)
.end()
// Server configuration
validate("config.server.port")
.required()
.min(1024)
.max(65535)
.end()
validate("config.server.maxThreads")
.required()
.min(1)
.max(1000)
.end()
// Feature flags
validateAll("config.features.*")
.custom(
predicate = { it is Boolean },
message = "Feature flags must be boolean values"
)
.end()
// Logging levels
validate("config.logging.level")
.required()
.allowedValues("DEBUG", "INFO", "WARN", "ERROR")
.end()
// File paths
validateAll("config.paths.*")
.custom(
predicate = { path ->
path?.toString()?.let {
java.io.File(it).exists()
} ?: false
},
message = "Path must exist on the filesystem"
)
.asWarning() // Only warn for non-existent paths
.end()
}

