Behavior dictated by case-by-case configuration

The problem

We have all seen code that looks like this:
12345
if (condition) {
foo();
} else {
bar();
}

It's the standard "if" block, backbone of programming and logical operations. You've probably even seen this, and have noticed you now need to add a third case.
1234567
if (condition) {
foo();
} else if (condition2) {
foobar();
} else {
bar();
}

Hell, you might even have had to expand it even more, and you ended up doing a switch case. This is fine, you could even clean that up with some early return conditions or ternaries to make it look nice.

However, what do you do when you have ten, twenty, or hundreds of different cases?

Well, usually the cases are not that different. Perhaps the methods can take in some arguments to simplify the conditional, and the method might be able to easily handle that case. That's usually a good start until the methods themselves now have a ton of conditionals. It sort of just pushes the trash into the closet.

Usually the conditionals have to be somewhere, I could never myself come up with a perfect generic method for ever case. There has to be a balance. So how did I tackle this issue when I was put as a Lead developer role to develop a service that has to theoretically handle connecting and running through API flows of 100s of different providers, which all have different requirements on parameters, bodies, and return different responses and behave differently?

Telling the code what it should do and what it should say

A rather unclear sentence, but let me elaborate.

The short story is we kept all of these providers in a database, to connect sessions to. These providers have roughly 7-12 different endpoints each to go through their whole flow. They all have different endpoints, parameters, content-types, different steps, and requirements. We knew that developing a whole flow would take a long time for every one of these cases, hell, we have to set up several of these flows for each provider as there's cases where the approach, and the type of user in question.

So we decided that we will spend the time at the start to set up a system that will save us time in the long run. Configuration dictated logic. We first sorted the providers into a few major different connection approaches. These dictate major differences between these providers, for when it isn't feasible to combine them. Then for each of these approaches, we developed methods that read configuration for each provider to hydrate our requests, and to explain how to parse the responses.

For a rough example, we might have a call that is supposed to get account information from a provider for a user. Some of these things will remain relatively consistent. It is most of the time a GET call. It usually requires an Oauth access token in the Authorization header, and we most likely need to supply API key/client id+client secret.
If I were to present a bit of psuedo code with Ruby flavor, here's what a call like that could look like:
123456789101112131415161718192021222324
def access_token
# Gets the configuration from the provider for getting an account
config = session.provider.configuration.fetch("token", {})

# A utility class that takes in the configuration and the session,
# and handles parsing the config.
request_utility = RequestUtility.new(session, config)

# Builds the url from the endpoint, and parameters which may include dynamic values(!)
url = request_utility.url
# Gets the headers from configuration, and puts in default values.
headers = request_utility.headers({
"content-type" => "application/x-www-form-urlencoded"
})
# Same but for the body
body = request_utility.body({
"scopes": "ACCOUNTS",
"code" => session.authorization.code
})
# Sends to method that handles the request.
response = ApiRequest.new({url: url, headers: headers}).post(body)

# ... handle the response
end


Well, with that shown, how would a configuration look like to complement this call?
1234567891011121314
// ... A lot of configuration before and after this.
"token": {
"endpoint": "https://api.fake-provider.com/psd2/token"
"headers": {
"Date": "Time.now()" // Will be parsed as actual code in the RequestUtility class.
},
"body": {
"scopes": "ACCOUNTS, BALANCES" // Normally not in this specific call, just for reference.
"client_id": "client_id()" // Same here, will take it from .env
"client_secret": "client_secret()" // ditto
"psu_id": "psu_id()" // A value that relates to the end user.
}
}


Now first of all, normally scopes are not part of the call to get an access token, but it's here just to showcase it.
So to break it down a bit, the method will get the configuration, and then use the Utility method which will parse the configuration and combine it with the default values. If something is defined in the configuration, it will overwrite the default values. For example, the default value for "scopes" is "ACCOUNTS", but configuration changes it to "ACCOUNTS, BALANCES". There's also meta programming, by parsing strings that follow a pattern, in this case it ends with "()", it will attempt to call a method that exists in an enclosed class with that name. It also allows for arguments.

This essentially changes the development from writing Ruby on rails(in our case), to writing JSON. The amount of code written is significantly lower, and it is easier to update parts of the code for all the providers, instead of going into 100s of different methods to update them.

Wrap

It's not revolutionary, and it isn't a new concept, but I think it is something to look into for quite a few cases. For example, customizable themes for individual users.

I want to revisit this blog post in the future with a more elaborate example, but for now I hope it roughly gives an idea. There's a lot of security concerns that has to be taken into account, which I do not touch on here. Please do not implement meta programming into your service without understanding the risks.