
Custom DSL in Scala
select (Case_.caseNumber :: (Case_.account :/ Account_.accountNumber) :: Case_.`engineeringReqNumberC` :: HNil) from Case_ where ( (Case_.caseNumber :== "5555") and (Case_.`createdDate` :== Calendar.getInstance()) )
SELECT CaseNumber, Account.AccountNumber, EngineeringReqNumber__c FROM Case WHERE (((CaseNumber = '5555')) AND (CreatedDate = '2015-06-08T10:01:42+02:00'))
select(Case_.caseNumber :: 4 :: HNil) from Case_
error: type mismatch; [ERROR] found : shapeless.::[com.github.akovari.typesafeSalesforce.model.Field[String],shapeless.::[Int,shapeless.HNil]] [ERROR] required: com.github.akovari.typesafeSalesforce.query.ColumnList[?] [ERROR] select(Case_.caseNumber :: 4 :: HNil) from Case_
Case_.caseNumber :== 4
error: type mismatch; [ERROR] found : Int(4) [ERROR] required: com.github.akovari.typesafeSalesforce.query.Field[String] [ERROR] Case_.caseNumber :== 4
Implementation
Web Serivces and code generation
<complexType name="Case"> <complexContent> <extension base="ens:sObject"> <sequence> <element name="Account" nillable="true" minOccurs="0" type="ens:Account"/> <element name="AccountId" nillable="true" minOccurs="0" type="tns:ID"/> <element name="ActivityHistories" nillable="true" minOccurs="0" type="tns:QueryResult"/> <element name="Asset" nillable="true" minOccurs="0" type="ens:Asset"/> <element name="AssetId" nillable="true" minOccurs="0" type="tns:ID"/> ...
- since we’re in scala we could use Macros. The problem is that this client is actually Java and not scala code so using Macros doesn’t seem very natural, also writing Macros still looks like a little bit of magic, I know, quasi-qoutes make it much nicer but still - I have no experience with Macros just yet. Maybe in the future together with the ScalaXB this could work.
- Java compile time annotation processor combined with a simple template library such as velocity to parse JAXB annotations and generate meta model classes
- other approaches to compile time or runtime(reflection) code generation
- In the binding.xml we annotate all SalesForce object with our custom annotation
<jxb:bindings node="//xs:complexType"> <annox:annotate target="class"> <annox:annotate annox:class="com.github.akovari.typesafeSalesforce.annotations.SalesForceEntity"/> </annox:annotate>
- And then we process it by an annotation processor and generate the meta model classes.
@SupportedAnnotationTypes({"com.github.akovari.typesafeSalesforce.annotations.SalesForceEntity"}) @SupportedSourceVersion(SourceVersion.RELEASE_8) public class SalesForceAnnotationProcessor extends AbstractProcessor
DSL Construction
import shapeless._ case class SelectQuery[C <: HList](columns: ColumnList[C], entities: Seq[Entity] = Seq.empty, filter: Option[Filter] = None, orders: Option[OrderList[_ <: HList]], groupBys: Seq[GroupBy[_]] = Seq.empty, limit: Option[Limit] = None)
object SelectQuery { type SelectableColumn[T, C <: HList] = Either[SimpleColumn[T], EmbeddedSelectColumn[T, C]] def select[C <: HList](columns: ColumnList[C]) = SelectQueryStep(columns) sealed trait QueryStep[C <: HList] { val query: SelectQuery[C] override def toString = query.toString } case class SelectQueryStep[C <: HList](columns: ColumnList[C]) extends QueryStep[C] { override val query = SelectQuery(columns = columns, orders = None) def from(entities: Entity*): FromQueryStep[C] = FromQueryStep(query, entities) } case class FromQueryStep[C <: HList](sq: SelectQuery[C], entities: Seq[Entity]) extends QueryStep[C] { override val query = SelectQuery(columns = sq.columns, entities = entities, orders = None) def where(filter: Filter): WhereQueryStep[C] = WhereQueryStep(query, filter) def orderBy[O <: HList](orders: OrderList[O]): OrderQueryStep[C, O] = OrderQueryStep(query, orders) def groupBy(groupBys: GroupBy[_]*): GroupByQueryStep[C] = GroupByQueryStep(query, groupBys) def limit(limit: Limit): LimitQueryStep[C] = LimitQueryStep(query, limit) } case class WhereQueryStep[C <: HList](sq: SelectQuery[C], filter: Filter) extends QueryStep[C] { override val query = SelectQuery(columns = sq.columns, entities = sq.entities, filter = Some(filter), orders = None) def orderBy[O <: HList](orders: OrderList[O]): OrderQueryStep[C, O] = OrderQueryStep(query, orders) def groupBy(groupBys: GroupBy[_]*): GroupByQueryStep[C] = GroupByQueryStep(query, groupBys) def limit(limit: Limit): LimitQueryStep[C] = LimitQueryStep(query, limit) } case class OrderQueryStep[C <: HList, O <: HList](sq: SelectQuery[C], orders: OrderList[O]) extends QueryStep[C] { override val query = SelectQuery(columns = sq.columns, entities = sq.entities, filter = sq.filter, orders = Some(orders)) def groupBy(groupBys: GroupBy[_]*): GroupByQueryStep[C] = GroupByQueryStep(query, groupBys) def limit(limit: Limit): LimitQueryStep[C] = LimitQueryStep(query, limit) } case class GroupByQueryStep[C <: HList](sq: SelectQuery[C], groupBys: Seq[GroupBy[_]]) extends QueryStep[C] { override val query = SelectQuery(columns = sq.columns, entities = sq.entities, filter = sq.filter, orders = sq.orders, groupBys = groupBys) def limit(limit: Limit): LimitQueryStep[C] = LimitQueryStep(query, limit) } case class LimitQueryStep[C <: HList](sq: SelectQuery[C], limit: Limit) extends QueryStep[C] { override val query = SelectQuery(columns = sq.columns, entities = sq.entities, filter = sq.filter, orders = sq.orders, groupBys = sq.groupBys, limit = Some(limit)) } }
case class ColumnList[L <: HList](l : L) object ColumnPoly extends Poly1 { implicit def caseField[T, S <% ModelField[T]] = at[ModelField[T]](f => fieldToColumn(f).toString) implicit def caseSelectQueryStep[T, C <: HList, QS <: SelectQueryStep[C], S <% QS] = at[SelectQueryStep[C]](qs => EmbeddedSelectColumn[T, C](qs).toString) implicit def caseFromQueryStep[T, C <: HList, QS <: FromQueryStep[C], S <% QS] = at[FromQueryStep[C]](qs => EmbeddedSelectColumn[T, C](qs).toString) implicit def caseWhereQueryStep[T, C <: HList, QS <: WhereQueryStep[C], S <% QS] = at[WhereQueryStep[C]](qs => EmbeddedSelectColumn[T, C](qs).toString) implicit def caseOrderQueryStep[T, C <: HList, O <: HList, QS <: OrderQueryStep[C, O], S <% QS] = at[OrderQueryStep[C, O]](qs => EmbeddedSelectColumn[T, C](qs).toString) implicit def caseGroupByQueryStep[T, C <: HList, QS <: GroupByQueryStep[C], S <% QS] = at[GroupByQueryStep[C]](qs => EmbeddedSelectColumn[T, C](qs).toString) implicit def caseLimitQueryStep[T, C <: HList, QS <: LimitQueryStep[C], S <% QS] = at[LimitQueryStep[C]](qs => EmbeddedSelectColumn[T, C](qs).toString) implicit def caseSimpleColumn[T, SC <: SimpleColumn[T], S <% SC] = at[SimpleColumn[T]](_.toString) implicit def caseEmbeddedSelectColumn[T, C <: HList, SC <: EmbeddedSelectColumn[T, C], S <% SC] = at[EmbeddedSelectColumn[T, C]](_.toString) } implicit def hlistToColumnList[L <: HList, M <: HList](a: L)(implicit mapper: Mapper.Aux[ColumnPoly.type, L, M]): ColumnList[M] = new ColumnList[M]((a map ColumnPoly))
object OrderPoly extends Poly1 { implicit def caseAsc[T, S <% AscendingOrder[T]] = at[AscendingOrder[T]](_.toString) implicit def caseDesc[T, S <% DescendingOrder[T]] = at[DescendingOrder[T]](_.toString) } implicit def hlistToOrderList[L <: HList, M <: HList](a: L)(implicit mapper: Mapper.Aux[OrderPoly.type, L, M]): OrderList[M] = new OrderList[M]((a map OrderPoly))
Conclusion
Conclusion
That is more or less all there is to it. We have shown how to enhance a Java only interface and extend it with additional type safety provided by the Scala ecosystem. We have an intuitive interface for writing SOQL queries that also gives us a lots of safety and simplicity thanks to the IDE code completion when typing new queries.
- Aggregated columns are not type safe - These should only work for Number fields.
- Improve Case_.account : / Account_.accountNumber - only Account_ fields should be allowed after Case_.account
- Generalize the Build process to work with external WSDLs
- Add support for Insert, Update, Upsert and Delete queries
Last updated: February 23, 2024