A DSL in 5 Languages

We provide client libraries for our customers in 5 programming languages: Ruby, Python, PHP, C# and Java. Each library is built to help our customers make requests, parse responses and retrieve data from our gateway. We hope they make integrating with Braintree simple and intuitive.

Maintaining 5 client libraries means writing essentially the same functionality in 5 languages. In many cases this just means differences in syntax. However, some features are complicated enough to warrant a slightly different approach in each library.

One example is transaction searching. Because searching can be a bit complex, we decided to create a Domain Specific Language (DSL) in each of the five libraries.

The Problem

We wanted transaction searching that was both easy to read and deep enough to perform complicated queries. Specifically, we wanted to allow searching on 3 different types of fields:

  • Text fields - queried for exact match, no match, beginning of string, end of string and substring
  • Multiple value fields - queried with a set of predefined values and will return all records matching any of the given values
  • Range fields - queried with a lower bound, an upper bound or both (all are inclusive)

A collection of resources matching all criteria will be returned.

Example Search Criteria

The code samples below assume the user wants to search for transactions meeting the following criteria:

  • The order id starts with “a2d”
  • The customer website ends with “.com”
  • The billing first name equals “John”
  • The status is either Authorized or Settled
  • The amount is between 10 and 20 dollars

Ruby

Strategy

In Ruby, the search method yields a search object to a block. This object contains the necessary methods for building search criteria. The block is then executed and the request is built from the result. Ruby also overloads the ==, !=, >= and <= operators on the text and range search fields. We think this improves readability and reduces syntatic noise.

Example

collection = Braintree::Transaction.search do |search|
  search.order_id.starts_with "a2d"
  search.customer_website.ends_with ".com"
  search.billing_first_name == "John"  search.status.in(
    Braintree::Transaction::Status::Authorized,
    Braintree::Transaction::Status::Settled
  )
  search.amount.between "10.00", "20.00"
end

collection.each do |transaction|
  puts transaction.id
end  

Strengths

  • No parentheses necessary on method calls creates a more readable syntax
  • Single step for creating the request and performing the search
  • Operator overloading improves readability

Weaknesses

  • Repetition of the search block variable

Python

Strategy

Our Python solution uses a different approach because the language lacks a block syntax and multi-line lambdas. The search method expects a list of objects representing search criteria. Each of these objects is built inline using readable method names. The search method is then able to iterate over the provided objects to build the search request.

Like Ruby, this implementation overloads the operators ==, !=, > and < for operations on the text and range fields.

Example

collection = Transaction.search([
    TransactionSearch.order_id.starts_with("a2d"),
    TransactionSearch.customer_website.ends_with(".com"),
    TransactionSearch.billing_first_name == "John",
    TransactionSearch.status.in_list([
        Transaction.Status.Authorized,
        Transaction.Status.Settled
    ]),
    TransactionSearch.amount.between("10.00", "20.00")
])

for transaction in collection.items:
    print transaction.id

Strengths

  • Single step for creating the request and performing the search
  • Easy to dynamically create criteria
  • Operator overloading improves readability

Weaknesses

  • Repetition of TransactionSearch class name
  • Literal list used as argument

PHP

Strategy

The PHP implementation is similar to the Python solution described above but feels less readable. Again, the search method expects a list of search criteria objects and these objects are created inline during the method call. However, the :: syntax for class method invocation along with the -> operator for instance method invocation make the code a bit more noisy.

Example

$collection = Braintree_Transaction::search(array(
    Braintree_TransactionSearch::orderId()->startsWith('a2d'),
    Braintree_TransactionSearch::customerWebsite()->endsWith('.com'),
    Braintree_TransactionSearch::billingFirstName()->is('John'),
    Braintree_TransactionSearch::status()->in(array(
        Braintree_Transaction::AUTHORIZED,
        Braintree_Transaction::SETTLED
    )),
    Braintree_TransactionSearch::amount()->between('10.00', '20.00')
));

foreach($collection as $transaction) {
    print_r($transaction->id);
}

Strengths

  • Single step for creating the request and performing the search
  • Easy to dynamically create criteria

Weaknesses

  • Repetition of Braintree_TransactionSearch class name
  • Literal array used as argument

C#

Strategy

In C# we used a fluent interface as the basis for our DSL because it is the idiomatic choice given the language. The fluent interface is used to build a TransactionSearchRequest object that represents the criteria of the search. Method chaining is used to specify the search criteria in pairs: the name of the field to search on followed by the restrictions on the value of that field. These name/restriction pairs are also chained together.

Notice that now there are two steps to performing the search. We need to build the search criteria before actually preforming the search. C# does allow us to avoid parentheses after the field names through the use of properties.

The TransactionSearchRequest is then passed to the search method which returns a collection.

Example

TransactionSearchRequest searchRequest = new TransactionSearchRequest().
    OrderId.StartsWith("a2d").
    CustomerWebsite.EndsWith(".com").
    BillingFirstName.Is("John").
    Status.IncludedIn(
        TransactionStatus.AUTHORIZED,
        TransactionStatus.SETTLED
    ).
    Amount.Between(10.00M, 20.00M);

ResourceCollection>Transaction< collection = gateway.Transaction.Search(
    searchRequest
);

foreach (Transaction transaction in collection)
{
    Console.WriteLine(transaction.Id);
}

Strengths

  • No parentheses are necessary after specifying the name of the search field because properties are used
  • Fluent interface provides a concise syntax for describing search criteria

Weaknesses

  • Separate steps for creating the request and performing the search

Java

Strategy

The Java DSL is also based on a fluent interface. Again, the fluent interface is used to build a TransactionSearchRequest via method chaining.

In Java we see an extra set of parentheses after the search field name because of the lack of properties. We feel this detracts from the readability of the code.

Example

TransactionSearchRequest searchRequest = new TransactionSearchRequest().
    orderId().startsWith("a2d").
    customerWebsite().endsWith(".com").
    billingFirstName().is("John").
    status().in(
        Transaction.Status.AUTHORIZED,
        Transaction.Status.SETTLED
    ).
    amount().between(new BigDecimal("10.00"), new BigDecimal("20.00"));

ResourceCollection collection = gateway.transaction().search(
    searchRequest
);

for (Transaction transaction : collection) {
    System.out.println(transaction.getId());
}

Strengths

  • Fluent interface provides a concise syntax for describing search criteria

Weaknesses

  • Parentheses are necessary after specifying the name of the search field
  • Separate steps for creating the request and performing the search

Final Thoughts

In general, there were 3 types of DSL implementations: fluent interface, readable inline method arguments and blocks. We found fluent interfaces the most effective approach for the static languages. For Python and PHP, readable inline method arguments seemed to be the most idiomatic. The Ruby community tends to prefer block based DSLs and we chose this approach for our Ruby implementation.

It was an incredibly valuable experience to solve the same problem across 5 different languages. Although the results are quite different, we think each solution is robust and readable. And, more importantly, working in these languages helped us to appreciate the strengths and weaknesses of each from a relatively unbiased perspective.

If you’re a developer working in one the these languages, we’d love to hear your feedback. How would you approach this problem in your language of choice?

***
Drew Olson Drew is a Principal Engineer at Braintree. More posts by this author

You Might Also Like