Skip to main content

Step 1: Understand the Requirement

Let’s say we want to add a new feature to convert temperature units (Celsius to Fahrenheit and vice versa).

Step 2: Identify Where to Extend

We need to create:
  1. A new TemperatureConversionRule class that implements MappingRule
  2. A new builder to construct this rule
  3. Extension methods on TargetBuilder to use this feature

Step 3: Implement the Mapping Rule

package xyz.mahmoudahmed.dsl.custom

import xyz.mahmoudahmed.adapter.DataNode
import xyz.mahmoudahmed.dsl.core.MappingContext
import xyz.mahmoudahmed.dsl.core.MappingRule
import xyz.mahmoudahmed.dsl.core.SimpleMapping

/**
 * Rule for converting temperature between different units.
 */
class TemperatureConversionRule(
    private val sourcePath: String,
    private val targetPath: String,
    private val sourceUnit: TemperatureUnit,
    private val targetUnit: TemperatureUnit
) : MappingRule {
    
    override fun apply(context: MappingContext, target: Any) {
        // Get the source value
        val sourceValue = context.getValueByPath(sourcePath)
        
        // Convert only if we have a number
        if (sourceValue is Number) {
            val sourceTemp = sourceValue.toDouble()
            val convertedTemp = convertTemperature(sourceTemp, sourceUnit, targetUnit)
            
            // Set the converted value
            if (target is DataNode.ObjectNode) {
                SimpleMapping.setValueInDataNode(target, targetPath, convertedTemp)
            }
        }
    }
    
    private fun convertTemperature(
        value: Double, 
        fromUnit: TemperatureUnit, 
        toUnit: TemperatureUnit
    ): Double {
        return when {
            fromUnit == toUnit -> value
            fromUnit == TemperatureUnit.CELSIUS && toUnit == TemperatureUnit.FAHRENHEIT -> 
                (value * 9/5) + 32
            fromUnit == TemperatureUnit.FAHRENHEIT && toUnit == TemperatureUnit.CELSIUS -> 
                (value - 32) * 5/9
            fromUnit == TemperatureUnit.CELSIUS && toUnit == TemperatureUnit.KELVIN -> 
                value + 273.15
            fromUnit == TemperatureUnit.KELVIN && toUnit == TemperatureUnit.CELSIUS -> 
                value - 273.15
            fromUnit == TemperatureUnit.FAHRENHEIT && toUnit == TemperatureUnit.KELVIN -> 
                (value - 32) * 5/9 + 273.15
            fromUnit == TemperatureUnit.KELVIN && toUnit == TemperatureUnit.FAHRENHEIT -> 
                (value - 273.15) * 9/5 + 32
            else -> throw IllegalArgumentException("Unsupported conversion: $fromUnit to $toUnit")
        }
    }
}

/**
 * Temperature units supported by the converter.
 */
enum class TemperatureUnit {
    CELSIUS,
    FAHRENHEIT,
    KELVIN
}

Step 4: Create a Builder for the New Feature

package xyz.mahmoudahmed.dsl.custom

import xyz.mahmoudahmed.dsl.builders.TargetBuilder

/**
 * Builder for configuring temperature conversion.
 */
class TemperatureConversionBuilder(
    private val parent: TargetBuilder,
    private val sourcePath: String,
    private val sourceUnit: TemperatureUnit
) {
    /**
     * Specify the target temperature unit.
     *
     * @param targetUnit The unit to convert to
     * @return Builder for specifying the target path
     */
    fun to(targetUnit: TemperatureUnit): TemperatureTargetBuilder {
        return TemperatureTargetBuilder(parent, sourcePath, sourceUnit, targetUnit)
    }
}

/**
 * Builder for specifying the target path for temperature conversion.
 */
class TemperatureTargetBuilder(
    private val parent: TargetBuilder,
    private val sourcePath: String,
    private val sourceUnit: TemperatureUnit,
    private val targetUnit: TemperatureUnit
) {
    /**
     * Specify the target path where the converted temperature will be stored.
     *
     * @param targetPath The path in the target object
     * @return The parent target builder
     */
    fun at(targetPath: String): TargetBuilder {
        val rule = TemperatureConversionRule(sourcePath, targetPath, sourceUnit, targetUnit)
        parent.addRule(rule)
        return parent
    }
}

Step 5: Extend TargetBuilder with the New Feature

package xyz.mahmoudahmed.dsl.builders

import xyz.mahmoudahmed.dsl.custom.TemperatureConversionBuilder
import xyz.mahmoudahmed.dsl.custom.TemperatureUnit

/**
 * Extension functions for TargetBuilder to support temperature conversion.
 */

/**
 * Starts a temperature conversion from Celsius.
 *
 * @param sourcePath The path to the temperature value in Celsius
 * @return Builder for configuring the temperature conversion
 */
fun TargetBuilder.convertCelsius(sourcePath: String): TemperatureConversionBuilder {
    return TemperatureConversionBuilder(this, sourcePath, TemperatureUnit.CELSIUS)
}

/**
 * Starts a temperature conversion from Fahrenheit.
 *
 * @param sourcePath The path to the temperature value in Fahrenheit
 * @return Builder for configuring the temperature conversion
 */
fun TargetBuilder.convertFahrenheit(sourcePath: String): TemperatureConversionBuilder {
    return TemperatureConversionBuilder(this, sourcePath, TemperatureUnit.FAHRENHEIT)
}

/**
 * Starts a temperature conversion from Kelvin.
 *
 * @param sourcePath The path to the temperature value in Kelvin
 * @return Builder for configuring the temperature conversion
 */
fun TargetBuilder.convertKelvin(sourcePath: String): TemperatureConversionBuilder {
    return TemperatureConversionBuilder(this, sourcePath, TemperatureUnit.KELVIN)
}

Step 6: Use the New Feature

// Create a mapping that uses the temperature conversion
val weatherMapping = Platymap.flow("weatherData")
    .to("processedWeather")
    .map("location.name").to("city")
    .map("location.country").to("country")
    
    // Use our new temperature conversion feature
    .convertCelsius("temperature.current")
        .to(TemperatureUnit.FAHRENHEIT)
        .at("temperature.fahrenheit")
        
    .convertFahrenheit("temperature.fahrenheit")
        .to(TemperatureUnit.KELVIN)
        .at("temperature.kelvin")
        
    .build()

// Sample weather data
val weatherJson = """
{
    "location": {
        "name": "London",
        "country": "UK",
        "coordinates": {
            "latitude": 51.5074,
            "longitude": -0.1278
        }
    },
    "temperature": {
        "current": 22.5,
        "min": 19.2,
        "max": 24.8
    },
    "conditions": "Partly Cloudy",
    "humidity": 65,
    "wind": {
        "speed": 12,
        "direction": "NW"
    }
}
"""

// Execute the mapping
val result = weatherMapping.executeToJson(weatherJson)
println(result)
This would produce:
{
  "city": "London",
  "country": "UK",
  "temperature": {
    "fahrenheit": 72.5,
    "kelvin": 295.65
  }
}

Step 7: Add Tests

@Test
fun testTemperatureConversion() {
    // Create a simple mapping that uses temperature conversion
    val mapping = Platymap.flow("source")
        .to("target")
        .convertCelsius("celsius")
            .to(TemperatureUnit.FAHRENHEIT)
            .at("fahrenheit")
        .convertFahrenheit("fahrenheit")
            .to(TemperatureUnit.CELSIUS)
            .at("backToCelsius")
        .build()
    
    // Test data
    val sourceJson = """
    {
        "celsius": 25.0
    }
    """
    
    // Execute the mapping
    val result = mapping.execute(sourceJson)
    
    // Verify results
    val targetNode = result as DataNode.ObjectNode
    val fahrenheit = targetNode.get("fahrenheit") as DataNode.NumberValue
    val backToCelsius = targetNode.get("backToCelsius") as DataNode.NumberValue
    
    assertEquals(77.0, fahrenheit.value.toDouble(), 0.01)
    assertEquals(25.0, backToCelsius.value.toDouble(), 0.01)
}