Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

DSL in scala using case classes

My use case has case classes something like

 case class Address(name:String,pincode:String){
    override def toString =name +"=" +pincode
  }

 case class Department(name:String){
   override def toString =name
 }

 case class emp(address:Address,department:Department)

I want to create a DSL like below.Can anyone share the links about how to create a DSL and any suggestions to achieve the below.

 emp.withAddress("abc","12222").withDepartment("HR")

Update: Actual use case class may have more fields close to 20. I want to avoid redudancy of code

like image 680
coder25 Avatar asked Aug 12 '17 12:08

coder25


4 Answers

I created a DSL using reflection so that we don't need to add every field to it.

Disclamer: This DSL is extremely weakly typed and I did it just for fun. I don't really think this is a good approach in Scala.

scala> create an Employee where "homeAddress" is Address("a", "b") and "department" is Department("c") and that_s it
res0: Employee = Employee(a=b,null,c)

scala> create an Employee where "workAddress" is Address("w", "x") and "homeAddress" is Address("y", "z") and that_s it
res1: Employee = Employee(y=z,w=x,null)

scala> create a Customer where "address" is Address("a", "b") and "age" is 900 and that_s it
res0: Customer = Customer(a=b,900)

The last example is the equivalent of writing:

create.a(Customer).where("address").is(Address("a", "b")).and("age").is(900).and(that_s).it

A way of writing DSLs in Scala and avoid parentheses and the dot is by following this pattern:

object.method(parameter).method(parameter)...

Here is the source:

// DSL

object create {
  def an(t: Employee.type) = new ModelDSL(Employee(null, null, null))
  def a(t: Customer.type) = new ModelDSL(Customer(null, 0))
}

object that_s

class ModelDSL[T](model: T) {
  def where(field: String): ValueDSL[ModelDSL2[T], Any] = new ValueDSL(value => {
    val f = model.getClass.getDeclaredField(field)
    f.setAccessible(true)
    f.set(model, value)
    new ModelDSL2[T](model)
  })

  def and(t: that_s.type) = new { def it = model }
}

class ModelDSL2[T](model: T) {
  def and(field: String) = new ModelDSL(model).where(field)

  def and(t: that_s.type) = new { def it = model }
}

class ValueDSL[T, V](callback: V => T) {
  def is(value: V): T = callback(value)
}

// Models

case class Employee(homeAddress: Address, workAddress: Address, department: Department)

case class Customer(address: Address, age: Int)

case class Address(name: String, pincode: String) {
  override def toString = name + "=" + pincode
}

case class Department(name: String) {
  override def toString = name
}
like image 159
Helder Pereira Avatar answered Nov 14 '22 11:11

Helder Pereira


I really don't think you need the builder pattern in Scala. Just give your case class reasonable defaults and use the copy method.

i.e.:

employee.copy(address = Address("abc","12222"), 
              department = Department("HR"))

You could also use an immutable builder:

case class EmployeeBuilder(address:Address = Address("", ""),department:Department = Department("")) {
  def build = emp(address, department)
  def withAddress(address: Address) = copy(address = address)
  def withDepartment(department: Department) = copy(department = department)
}

object EmployeeBuilder {
  def withAddress(address: Address) = EmployeeBuilder().copy(address = address)
  def withDepartment(department: Department) = EmployeeBuilder().copy(department = department)
}
like image 32
Luka Jacobowitz Avatar answered Nov 14 '22 12:11

Luka Jacobowitz


You could do

object emp {
  def builder = new Builder(None, None)

  case class Builder(address: Option[Address], department: Option[Department]) {
    def withDepartment(name:String) = {
      val dept = Department(name)
      this.copy(department = Some(dept))
    }
    def withAddress(name:String, pincode:String) = {
      val addr = Address(name, pincode)
      this.copy(address = Some(addr))
    }
    def build = (address, department) match {
      case (Some(a), Some(d)) => new emp(a, d)
      case (None, _) => throw new IllegalStateException("Address not provided")
      case _ => throw new IllegalStateException("Department not provided")
    }
  }
}

and use it as emp.builder.withAddress("abc","12222").withDepartment("HR").build().

like image 23
Alexey Romanov Avatar answered Nov 14 '22 11:11

Alexey Romanov


You don't need optional fields, copy, or the builder pattern (exactly), if you are willing to have the build always take the arguments in a particular order:

case class emp(address:Address,department:Department, id: Long)

object emp {
  def withAddress(name: String, pincode: String): WithDepartment =
    new WithDepartment(Address(name, pincode))

  final class WithDepartment(private val address: Address)
    extends AnyVal {
    def withDepartment(name: String): WithId =
      new WithId(address, Department(name))
  }

  final class WithId(address: Address, department: Department) {
    def withId(id: Long): emp = emp(address, department, id)
  }
}

emp.withAddress("abc","12222").withDepartment("HR").withId(1)

The idea here is that each emp parameter gets its own class which provides a method to get you to the next class, until the final one gives you an emp object. It's like currying but at the type level. As you can see I've added an extra parameter just as an example of how to extend the pattern past the first two parameters.

The nice thing about this approach is that, even if you're part-way through the build, the type you have so far will guide you to the next step. So if you have a WithDepartment so far, you know that the next argument you need to supply is a department name.

like image 33
Yawar Avatar answered Nov 14 '22 11:11

Yawar