SDKless

Content

About

SDKless is a specification for describing the usage requirements of APIs. The goal of SDKless is to allow developers to use one code library to consume all APIs, rather than incorporating a separate SDK for each API. This will help speed up development time, allow for cleaner code, make troubleshooting easier, and reduce the number of code files required by your application.

SDKless is most helpful when your application consumes multiple similar APIs. For example, let's say your application allows users to connect to any social networks they use so they can view all their activity in one place. Normally you would incorporate into your fileset, the SDK for the API of each social network you want to support. Then your code would call the SDK-specific method for each connected social network, and then reformat the API-specific output into something your application can use. Something kinda like this...

switch ($social_network) {
    case 'some-social-network':
        require_once('libraries/some-social-network.php');
        $api = new SomeSocialNetwork();
        $response = $api->getActivity();
        $posts = $response->data;
        $output = array();

        foreach ($posts as $post) {
            $output_item = array();
            $output_item['date'] = $contact['post_date'];
            $output_item['post'] = $contact['content']; 
            $output[] = $output_item;
        }

        break;
    case 'other-social-network':
        require_once('libraries/other-social-network.php');
        $api = new OtherSocialNetwork();
        $response = $api->pull_feed();
        $posts = $response['feed'];
        $output = array();

        foreach ($posts as $post) {
            $output_item = array();
            $output_item['date'] = $contact['date_added'];
            $output_item['content'] = $contact['feed_entry']; 
            $output[] = $output_item;
        }

        break;
}

Etc... for however many social networks you support.

With SDKless that would look more like this...

require_once('SDKless.php');
$sdkless = new SDKless($social_network);
$output = $sdkless->go('get_posts');

The way this works is that each API has a configuration file (or two; see customizing later) which the SDKless library uses to determine what needs to happen in order to get contacts from that API and return them in the desired format.

Now, granted, the code in the 1st example could just be put into a separate class and called like the 2nd example. But that is not what SDKless does. The SDKless class in the 2nd example (and included in this repository) has no API-specific code in it. No references to any APIs. It simply uses the configuration file for the specified API to know what to do.

And keep in mind that your project now only has one (or two; see customizing later) library files for SDKless, instead of one for each API you need to integrate with. And in many cases, an SDK can be made up of not just one, but several files. With SDKless, all APIs are consumed with the same code, which makes for quicker development, cleaner code, and easier troubleshooting.

This repository includes examples, in JSON configuration files, of this specification applied to several APIs. The hope is that these will be expanded upon, and more added, by the community, resulting in a mostly plug-n-play experience when consuming APIs. This repository also includes PHP and Python/Django demo implementations of SDKless.

The specification supports:

Documentation

Configuration Files

A configuration file (JSON) contains everything your code needs to consume an API. The included core library expects them to be located in a folder named "config" and named like "ServiceName.json", with "ServiceName" being passed to the constructor. The intention is that this file would be created and maintained by those in the community who are familiar with the particular API, and then developers using SDKless could simply incorporate it to their application and go. See custom configuration files for how to override these settings. The supported root level elements are...

{
    "base_uri": "https://api.abc.com/v4.2/",
    "authentication": {...},
    "common_endpoint_settings": {...},
    "endpoint_prerequisites": {...},
    "endpoints": {...},
}

Base URI

base_uri is concatenated with endpoint uris to form the actual uri to be used in the API call.

Authentication

The authentication element contains the steps needed to authenticate with the API. This can be used for OAuth1/2 as well as other non-standard flows. The steps are performed in order.

Authentication Step Settings

When processing authentication steps the output from one step is merged with the input for the next step, and parameter_maps and merge_maps are applied if specified. If the current step has no parameters, all output parameters from the previous step are processed and included in the current step. If the current step does have parameters, only output parameters from the previous step with keys matching the current step are processed and included.

Here is an example of OAuth2...

"authentication": {
    "steps": [
        {
            "type": "redirect",
            "uri": "https://api.abc.com/oauth",
            "parameters": {
            "client_id": "*|API-CLIENT-ID|*",
            "redirect_uri": "*|REDIRECT-URI|*"
        }
    },
        {
            "type": "endpoint",
            "endpoint": "access_token"
        }
    ]
}

Here is an example of OAuth1...

"authentication": {
    "oauth_header_parameters": {
        "oauth_callback": "*|OAUTH-CALLBACK|*",
        "oauth_consumer_key": "*|OAUTH-CONSUMER-KEY|*",
        "oauth_signature_method": "HMAC-SHA1",
        "oauth_version": "1.0",
        "oauth_consumer_secret": "*|OAUTH-CONSUMER-SECRET|*",
        "oauth_token": "*|OAUTH-TOKEN|*",
        "oauth_token_secret": "*|OAUTH-TOKEN-SECRET|*",
        "oauth_nonce": null,
        "oauth_timestamp": null,
        "oauth_signature": null
    },
    "steps": [
        {
            "type": "endpoint",
            "endpoint": "request_token"
        },
        {
            "type": "redirect",
            "uri": "https://api.abc.com/oauth/authenticate",
            "parameters": {
                "oauth_token": null
            }
        },
        {
            "type": "endpoint",
            "endpoint": "access_token"
        }
    ]
    },
    "common_endpoint_settings": {
        "all": {
            "include_oauth_header": true,
            "request_options": {
                "headers": {
                    "Authorization": "OAuth *|OAUTH-HEADER-PARAMS|*"
                }
            }
        }
    },
    "endpoints": {
        "request_token": {
            "uri": "https://api.abc.com/oauth/request_token",
        },
        "access_token": {
            "uri": "https://api.abc.com/oauth/access_token",
            "merge_maps": {
                "oauth_token": "OAUTH-TOKEN"
            }
        }
    }
}

Your global merge variables would look something like this...

$global_vars = array(
    'merge' => array(
        'OAUTH-CONSUMER-KEY' => '...',
        'OAUTH-CONSUMER-SECRET' => '...',
        'OAUTH-CALLBACK' => 'http://mysite.com/sdkless/auth.php',
    ),
);

Note that the "OAUTH-HEADER-PARAMS" merge variable is not specified. This one is automatically applied.

The auth_token and auth_token_secret parameters will be removed during the authentication process, as they are not specified (or needed). The access_token endpoint response should include the auth_token and auth_token_secret which you would then include in global merge variables as "OAUTH-TOKEN" and "OAUTH-TOKEN-SECRET" for subsequent API calls.

Common Endpoint Settings

Common endpoint settings are used for all endpoints unless the same setting exists in the endpoint itself. Method specific settings (get, post, etc..) take precedence over settings in the "all" section. See endpoints for supported settings.

"common_endpoint_settings": {
    "all": {
        "output_format": "json",
        "request_options": {
            "TIMEOUT": 60,
            "headers": {
                "Content-type": "application/x-www-form-urlencoded",
                "Accept": "application/json",
                "Authorization": "Bearer *|ACCESS-TOKEN|*"
            }
        }
    },
    "get": {
        "parameters": {
            "api_key": "*|API-KEY|*"
        }
    },
    "post": {
        "request_options": {
            "headers": {
                "Content-type": "application/json"
            }
        }
    }
}

Endpoint Prerequisites

endpoint_prerequisites specifies one or more endpoints to be called prior to the requested endpoint. This can be used for certain types of authentication, or for refreshing an access_token, for example. The response data from each prerequisite endpoint call can be merged into your config using the merge_maps setting. This allows output data from prerequisite endpoint calls to be used in any subsequent calls.

Endpoint Prerequisites Settings

endpoint

The name of the endpoint to call

protocol

Protocol is used to control what should happen with prerequisite endpoint response data. Currently only "cookie" is supported by the included PHP demo. The "cookie" protocol will set the COOKIEFILE and COOKIEJAR curl options required by some authentication schemes.

repeat

This specifies whether the prerequisite should be processed for every endpoint call (during the life of the script) or just the first.

merge_maps

See merge_maps

Here is an example of using prerequisites to refresh an access token before calling the requested endpoint. In this example we are using the custom config file, since how/when to refresh access tokens should be based on the needs of the application. Assuming our standard config looked something like this...

"common_endpoint_settings": {
    "all": {
        "request_options": {
            "headers": {
                "Authorization": :Bearer *|ACCESS-TOKEN|*"
            }
        }
    }
},
"endpoints": {
    "refresh_token": {
        "uri": "https://api.abc.com/oauth/token",
        "method": "post",
        "bypass_prerequisites": true,
        "parameters": {
            "grant_type": "refresh_token",
            "refresh_token": "*|REFRESH-TOKEN|*"
        }
    }
}

Our custom config would look something like this...

"global": {
    "parameter_maps": {
        "refresh_token": "REFRESH-TOKEN"
    },
    "set": {
        "endpoint_prerequisites": [
            {
                "endpoint": "refresh_token",
                "repeat": false,
                "merge_maps": {
                    "access_token": "ACCESS-TOKEN"
                }
            }
        ]
    }
}

So when we call any endpoint for this API, the refresh_token endpoint will be called first since it's specified in endpoint_prerequisites. We would pass a merge variable named "refresh_token", parameter_maps would update that to "REFRESH-TOKEN", and it's value would then replace "*|REFRESH-TOKEN|*" in the endpoint named "refresh_token". The output of that call contains a value named "access_token". merge_maps specifies to update that key to "ACCESS-TOKEN" and it's value would then replace "*ACCESS-TOKEN*" in common_endpoint_settings. Our requested endpoint would then be called, including the required access token. Using the included PHP library it would look like this...

$global_vars = array(
    'merge' => array(
        'refresh_token' => 'VALUE STORE IN DB',
    ),
);
$sdkless = new SDKless('ServiceName', $global_vars);
$output = $sdkless->go('DESIRED ENDPOINT NAME');

Endpoints

The endpoints element contains the key for each API endpoint along with their settings, like this...

"access_token": {
    "uri": "https://api.abc.com/oauth/access_token",
    "method": "get",
    "output_format": "query_string",
    "parameters": {
        "client_id": "*|CLIENT-ID|*",
        "client_secret": "*|CLIENT-SECRET|*",
        "redirect_uri": "*|REDIRECT-URI|*",
        "code": null
    }
},
"get_user_friends": {
    "uri": "*|USER-ID|*/friends",
    "method": "get"
},

The key (access_token, get_user_friends) is how your code will reference the desired endpoint.

Endpoint Settings

bypass_prerequisites

bypass_prerequisites is used to prevent endpoints from adhering to endpoint_prerequisites

request_options

request_options is a structure of key/value pairs, used to specify the options to be used when sending requests to the API. The included PHP demo uses curl and expects curl options (other than headers) to be specified without the "CURLOPT_" prefix. The included Django demo uses the requests library. So your request_options endpoint configuration might look like this...

"request_options": {
    "headers": {
        "Authorization: Bearer *|ACCESS-TOKEN|*"
    }
    "CONNECTTIMEOUT": 5,
    "TIMEOUT": 10,
    "SSL_VERIFYPEER": false,
    "SSL_VERIFYHOST": false
}

input_format

input_format specifies in what format the endpoint is expecting it's parameters

merge_maps

merge_maps is a structure of key/value pairs and is used by authentication and prerequisites. When processing an authentication step or a prerequisite endpoint, the output is merged into your config, allowing those values to be used in subsequent calls. The key is the output parameter name and the value is a string to be searched for and replaced with the actual value of the output parameter. As an example, the authentication flows for some APIs require an additional call after the access_token is retrieved, and this call requires the access_token itself. Something like this...

"authentication": {
    "steps": [
        {
            "type": "redirect",
            "uri": "https://login.abc.com/oauth2/authorize",
            "parameters": {
                "response_type": "code",
                "client_id": "*|CLIENT-ID|*",
                "redirect_uri": "*|REDIRECT-URI|*"
            }
        },
        {
            "type": "endpoint",
            "endpoint": "token"
        },
        {
            "type": "endpoint",
            "endpoint": "metadata"
        }
    ]
}

And the metadata endpoint would look like...

"metadata": {
    "uri": "https://login.abc.com/oauth2/metadata",
    "method": "get",
    "merge_maps": {
        "access_token": "ACCESS-TOKEN"
    },
    "request_options": {
        "headers": {
            "Authorization": "OAuth *|ACCESS-TOKEN|*"
        }
    }
}

The output of the token endpoint contains an access_token parameter. The value of that parameter then replaces *|ACCESS-TOKEN|* in the metadata endpoint call.

method (get, post)

* note: put and delete methods are supported in the included PHP demo by specifying CUSTOMREQUEST in endpoint->request_options or common_endpoint_settings->request_options

output

The output setting can be used in both standard and custom config files. In standard config files it would typically only be used to specify where any indication of error can be found. This would look like...

"output": {
    "error": {
        "location": ["error"]
    }
}

This specifies that the output should be checked in this location for the existence of a key named "error". See here for how location is processed.

In custom config files the structure would look like...

"output": {
    "data": {
        "format": "...",
        "location": [...],
        "items": {
            "locations": {...}
        },
        "key_filter": "..."
    },
    "filter": [
        {
            "search_key": "...",
            "search_value": "...",
            "return_key": "..."
        }
    ]
}

output -> data -> format (scalar, structure, iterable)

scalar indicates that output data is to be left as is. An example would be retrieving a record count, or an id after creating a record. It could also be used to retrieve an object such as a contact record, as long as the output did not need to be processed in any way. If paging is being done, the output of each call would be added to a final output array, though paging would be unlikely for scalar results.

structure is intended to be used when the output will contain an array or object. It works the same as scalar, but can also be processed using items functionality. An example would be retrieving a single contact and needing to specify where the email/first_name/last_name values are located.

iterable (default) works like structure, but when paging these will be merged to form a single output array.

output -> data -> location This array specifies where the desired output data is located in the API response. It will typically be used in the custom config file. Each item in the array represents a key in the response structure hierarchy. For example, the response of an API call may look like this...

{
    "total_count": 9,
    "data": {
        "contacts": [
            {
                "given_name": "John",
                "family_name": "Doe",
                "email_addresses": [
                    {
                        "address": "john@work.com",
                        "primary": 0
                    },
                    {
                        "address": "john@home.com",
                        "primary": 1
                    }
                ]
            },
            {
                "given_name": "Jane",
                "family_name": "Doe",
                "email_addresses": [
                    {
                        "address": "jane1@doe.com",
                        "primary": 1
                    },
                    {
                        "address": "jane2@doe.com",
                        "primary": 0
                    }
                ]
            }
        ],
        "other": [...]
    }
}

If you only wanted the array of contacts in your output you would specify that like so...

"output": {
    "data": {
        "location": ["data", "contacts"]
    }
}

output -> data -> items -> locations This is used when the API response is a structure (array or object) and you want to format the output for each item. Given the API response in the previous example, you may use something like this...

"output": {
    "data": {
        "location": ["data", "contacts"],
        "items": {
            "locations": {
                "email_address": [
                    "email_addresses",
                    {
                        "search_key": "primary",
                        "search_value": 1,
                        "return_key": "address"
                    }
                ],
                "first_name": "given_name",
                "last_name": "family_name"
            }
        }
    }
}

locations is a structure of key/value pairs. The key is the actual key we want used in our output. The value specifies where to find the value we want in the response.

If the value is scalar, then the response item is checked for that key. So the "first_name" item in our output would contain the value of "given_name" from the response, in this case "John" and "Jane".

If the value is an array, the response item hierarchy is searched as follows. For each item in the array, if the item is scalar, we drill down in the response item using that scalar value as the key. If the item is a structure, it must contain search_key, search_value, and return_key. Also, the current response item value must be a structure. Each item in that structure is tested with the search_key and search_value and, if it matches, the value corresponding to the return_key is returned.

So in the case of "email_address", the first item in the array is "email_addresses" which is scalar, so we drill down to that element in the response item. The next item is a structure containing the search parameters. So we loop through the items in the "email_addresses" array. Each item is checked to see if the "primary" (search_key) value is set to 1 (search_value), and if so the value associated with the "address" (return_key) is used.

So the output would be...

[
    {
        "first_name": "John",
        "last_name": "Doe",
        "email_address": "john@home.com"
    },
    {
        "first_name": "Jane",
        "last_name": "Doe",
        "email_address": "jane1@doe.com"
    }
]

output -> data -> key_filter

key_filter is used to remove unwanted results from a response. Currently only "numeric" is supported in the included PHP demo. When specified, the response structure is traversed and any items with non-numeric keys are removed. This is helpful in cases where the API response looks like this...

{
    "total": 9,
    0: {
        "email": "john@doe.com",
        "name": "John Doe"
    },
    1: {
        "email": "jane@doe.com",
        "name": Jane Doe"
    }
}

If your application is expecting a list of contacts, this will remove any non-numeric keyed values like "total".

output -> filter

The output filter setting is an array of one or more filters to be applied to the API response array. Each response item is checked and if the search_key value doesn't match the search_value, the item is removed. This is useful when the API doesn't provide filtering capabilities. If return_key is set, only the value for that key is returned, which is useful when you want to look up a value in one result of a set, for example, if you have a contacts email address and need to get their ID.

return_type is optional. When set to "boolean" the output will return true if a match is found and false if not. When set to "!boolean", the opposite is returned.

output_format

paging

The paging setting can be used in both standard and custom config files. In custom config files it would typically only be used to turn off paging for an endpoint (by setting it to false).

There are two types of paging supported; page_number and cursor.

Here is an example of page_number paging.

"paging": {
    "type": "page_number",
    "parameters": {
        "page_size": {
            "name": "per_page"
        },
        "page_number": {
            "name": "page",
            "base": 1
        }
    }
}

parameters -> page_size and parameters -> page_number are required. page_size -> name specifies the name (key) to be used in the API call to indicate the number of records per page. page_number -> name specifies the name (key) to be used to indicate the page number. page_number -> base specifies whether page numbering starts with 0 or 1.

Here is an example of cursor paging...

"paging": {
    "type": "cursor",
    "parameters": {
        "cursor": {
            "location": ["paging","next"]
        }
    }
}

parameters -> cursor -> location is required and indicates where in the response the next page cursor can be found. See here for how location is processed.

parameters

The parameters setting is used in both the standard and custom config files. It is a set of key/value pairs specifying the values that will be passed to the API endpoint. The standard config file will typically contain required parameters, either set to a required value, or set to null. Null indicates this parameter needs to be set by your variables or in your custom config file. They may also be set to a value containing a merge variable.

time_limit

time_limit is used to set the time limit for the entire script processing your endpoint request.

uri

This is the uri for the API endpoint call. If it is not a full uri, it will be appended to the base_uri.

Custom Configuration Files

Custom configuration files are used to override the settings in the standard configuration files for a given API. The included PHP library expects them to be located in a folder named "config", located in the same folder as the running script, and named like "ServiceName.custom.json". The intention is that developers using SDKless would create/modify these to suit the needs of their application, while the standard config files would be maintained by the community. This allows you to use the same code to call all of your APIs without having any API-specific logic cluttering up your code.

Custom configuration files support all of the same settings as standard config files, as well as...

array_set_templates

array_set_templates is used when calling an endpoint that accepts multiple items requiring the same format, like when adding multiple contacts to a CRM. Each contact will be reformatted to match the requirements of the API. For example, you may have data looking like this...

$contacts = array(
    array(
        'email_address' => 'john@doe.com',
        'first_name' => 'John',
        'last_name' => 'Doe',
    ),
    array(
        'email_address' => 'jane@doe.com',
        'first_name' => 'Jane',
        'last_name' => 'Doe',
    ),
);

And the API expects the contacts array to contain structures each formatted like this...

{
    "MemberEmail": "...",
    "MemberFirstName": "...",
    "MemberLastName": "..."
}

Your endpoint configuration would look like this...

"array_set_templates": {
    "contacts": {
        "MemberEmail": "email_address",
        "MemberFirstName": "first_name",
        "MemberLastName": "last_name"
    }
}

global

The global setting is another place to specify non-endpoint-specific values. The supported child settings are merge, set, and parameter_maps.

maps_to

This is used to specify the endpoint name used in the standard config file. This is useful for calling all of your integrated APIs using the same endpoint names. So, if for example you are integrating with multiple social networks, they would each have their own endpoint name (specified in their standard config file) for retrieving a feed, such as "get_status_updates", "feed", "export_posts", etc. You can call all of them with "pull_feed" by specifying that as the maps_to value.

parameter_maps

The parameter_maps setting is a series of key/value pairs that is used for updating parameters to the keys required by the API. This is applied when preparing authentication steps and endpoint variables.

This is useful for keeping all of your API calls consistent. Let's say you integrate with several CRMs and pull contacts from mailing lists. The API endpoints require an id parameter to specify the list you want. This may be named "id", "list_id", "group_id", etc. Using parameter_maps you can use "list_id for all of them. Let's say your config endpoint looked like this...

"get_people_from_group": {
    "uri": "groups/people.json"
    "parameters": {
        "group_id": null
    }
}

The endpoint in your custom config could look like this...

"pull_list_contacts": {
    "maps_to": "get_people_from_group",
    "parameter_maps": {
        "list_id": "group_id"
    }
}

So now you can pass in a "list_id" set variable and it will be passed to the API endpoint as the "group_id" in this case. These can also be used in conjunction with merge variables. Let's say, instead, your config endpoint looked like this...

"get_people_from_group": {
    "uri": "groups/*|GROUP-ID|*/people.json"
}

The endpoint in your custom config could look like this...

"pull_list_contacts": {
    "maps_to": "get_people_from_group",
    "parameter_maps": {
        "list_id": "GROUP-ID"
    }
}

You would now pass "list_id" as a merge variable and it would replace the *|GROUP-ID|* in the endpoint uri.

Variables

Variables can be incorporated into your API calls in several ways. Global**** variables (merge, set) are intended for values that are consistent across all endpoints, such as an API key . Endpoint variables (merge, set, array_set) are intended for endpoint-specific values. Local* variables are not incorporated into config files, but rather are intended for use by the code which processes the config files.

* the included PHP demo uses local variables for prerequisites using the cookie protocol, and for setting up non-standard paging parameters

** in the included PHP demo, merge variables are surrounded by an opening *| and closing |*

*** in the included PHP demo, global variables are passed to the constructor

Core Library

This repository contains an example implementation of the SDKless specification for PHP, in the "core" folder. The class to instantiate (or extend) is SDKless. This class incorporates all other classes in the library as needed. The constructor accepts the service name (API) and array of global variables as arguments.

The authenticate method accepts the step id, array of parameters from the previous step, and a "done" flag which should be set to true when the authentication is complete. Step id should start at 0. See the auth.php file included in this repository for example code for processing authentication.

The go method accepts the endpoint name, array of endpoint variables, and an array of local variables as arguments. See the index.php file included in this repository for example code for processing endpoints.

Extending the Core Library

This repository contains an example class for extending the SDKless class, called MySDKless. It is intended to be used to support API requirements that cannot be handled in configuration files. Some examples are...