Skip to main content

Architecture Overview

PlatyMap uses a multi-layered architecture:
  1. User Interface Layer: The DSL builders that provide a fluent API for creating mappings
  2. Rule Layer: The rules that define how data is transformed
  3. Execution Layer: The context and execution engine that applies the rules to data
  4. Utility Layer: Helper functions, extension methods, and registry services
The flow of data through the system is:
  1. The user creates a mapping using the DSL
  2. The DSL creates mapping rules
  3. The mapping is executed with source data
  4. The execution engine applies each rule to transform the data
  5. The transformed data is returned in the desired format
This architecture follows several design principles:
  • Separation of Concerns: Each component has a single responsibility
  • Immutability: Most objects are immutable after creation
  • Fluent Interface: Method chaining for a readable API
  • Composition over Inheritance: Rules are composed rather than inherited

Design Patterns Used

The PlatyMap library uses several design patterns that are important to understand:

Builder Pattern

The Builder pattern is used extensively throughout the library to construct complex objects through a fluent interface. Example:
val mapping = Platymap.flow("source")
    .withFormat(Format.JSON)
    .to("target")
    .map("name").to("fullName")
    .build()
In this example, flow(), withFormat(), to(), map(), and to() are all part of a builder chain that constructs a Mapping object. Key Builder Classes:
  • SourceBuilder
  • TargetBuilder
  • MappingBuilder
  • BranchBuilder
  • ForEachBuilder

Strategy Pattern

The Strategy pattern defines a family of algorithms, encapsulating each one, and making them interchangeable. In PlatyMap, each MappingRule is a strategy for transforming data. Example:
interface MappingRule {
    fun apply(context: MappingContext, target: Any)
}

class SimpleMapping(
    private val sourcePath: String,
    private val targetPath: String,
    private val transformation: ((Any) -> Any)?
) : MappingRule {
    override fun apply(context: MappingContext, target: Any) {
        // Implementation here
    }
}

Composite Pattern

The Composite pattern composes objects into tree structures to represent part-whole hierarchies. PlatyMap uses this pattern with nested rules and branches. Example:
// A composite rule that contains other rules
class BranchMapping(branches: List<ConditionalBranch>) : MappingRule {
    private val branches = ArrayList(branches)

    override fun apply(context: MappingContext, target: Any) {
        for (branch in branches) {
            if (branch.condition(context.sourceData)) {
                // Execute child rules in the branch
                for (rule in branch.actions) {
                    rule.apply(context, target)
                }
                break
            }
        }
    }
}

Command Pattern

The Command pattern encapsulates a request as an object, allowing parameterization of clients with different requests. Each mapping rule acts as a command that can be executed on data.

Factory Method Pattern

The Factory Method pattern defines an interface for creating an object, but lets subclasses decide which class to instantiate. PlatyMap uses factory methods to create different rule types. Example:
// Factory method to create a SimpleMapping
fun map(sourcePath: String): MappingBuilder {
    return MappingBuilder(this, sourcePath)
}

Registry Pattern

The Registry pattern provides a central place to store objects that need to be globally accessible. The FunctionRegistry in PlatyMap is an example:
object FunctionRegistry {
    private val functions = ConcurrentHashMap<String, MapFunction>()
    
    fun register(function: MapFunction) {
        functions[function.name] = function
    }
    
    fun get(name: String): MapFunction {
        return functions[name] ?: throw FunctionNotFoundException(name)
    }
    
    // More methods...
}

Chain of Responsibility Pattern

The Chain of Responsibility pattern passes a request along a chain of handlers. Each handler decides to process the request or pass it to the next handler. PlatyMap uses this in its builder chain, where each builder adds to the configuration and passes control to the next builder.

Package Structure

The library is organized into several packages:

xyz.mahmoudahmed.dsl.core

Contains the core interfaces and classes like MappingRule, MappingContext, and Mapping.

xyz.mahmoudahmed.dsl.builders

Contains the builder classes that form the DSL.

xyz.mahmoudahmed.dsl.functions

Contains the function registry and function-related classes.

xyz.mahmoudahmed.dsl.collections

Contains classes for handling collections like ForEachMapping.

xyz.mahmoudahmed.dsl.conditional

Contains classes for conditional branching like BranchBuilder and ConditionalBranch.

xyz.mahmoudahmed.dsl.structure

Contains classes for structural operations like nesting and flattening.

xyz.mahmoudahmed.dsl.bulk

Contains classes for bulk operations like BulkMappingRule.

xyz.mahmoudahmed.dsl.typed

Contains classes for type-safe mappings with Java beans.

xyz.mahmoudahmed.dsl.util

Contains utility classes like PathMatcher.

Core Components

MappingRule Interface

The fundamental building block of all mappings:
interface MappingRule {
    /**
     * Applies this mapping rule to transform source data to target data.
     *
     * @param context The context containing source data and variables
     * @param target The target object to populate
     */
    fun apply(context: MappingContext, target: Any)
}
All mapping rules implement this interface.

MappingContext Class

Holds the source data and provides access to values by path:
class MappingContext(val sourceData: Any) {
    private val variables = mutableMapOf<String, Any>()
    
    fun setVariable(name: String, value: Any) {
        variables[name] = value
    }
    
    fun getVariable(name: String): Any? {
        return variables[name]
    }
    
    fun getValueByPath(path: String): Any? {
        // Implementation that extracts values using the path
    }
}

Mapping Class

The main class that executes a mapping:
class Mapping(
    val sourceName: String,
    private val sourceFormat: Format,
    val targetName: String,
    private val targetFormat: Format,
    private val rules: List<MappingRule>
) {
    fun execute(sourceData: Any): Any {
        // Implementation that applies all rules to transform the data
    }
    
    // Other methods...
}

SimpleMapping Class

The most basic mapping rule that maps a single value:
class SimpleMapping(
    private val sourcePath: String,
    private val targetPath: String,
    private val transformation: ((Any) -> Any)?,
    private val condition: ((Any) -> Boolean)?
) : MappingRule {
    override fun apply(context: MappingContext, target: Any) {
        // Implementation that extracts a value and sets it in the target
    }
}

Platymap Object

The entry point to the DSL:
object Platymap {
    fun flow(source: String): SourceBuilder {
        return SourceBuilder(source)
    }
    
    fun <S> flow(sourceClass: Class<S>): TypedSourceBuilder<S> {
        return TypedSourceBuilder(sourceClass)
    }
    
    // Other methods...
}