Basically I want to be able to version the api at each release and when I update it still allow the old methods to work. Something like the following:

/api/1.0/foo
/api/1.1/foo

Behind the scenes they would either point to same function if no change or similar to the update routines where 1.0/foo would be a wrapper that changes information to 1.1 format and calls 1.1 so that the latest code always moves up and the backwards compatibility is just a set of "diffs".

Comments

gdd’s picture

I have not actually done this, but it should work.

1) Define two resources. 'foo_1.0' and 'foo_1.1'. This can be in two modules, or separately defined in one.
2) Define the callbacks in each resource. For the ones that stay the same, call the same callbacks. For different ones, call different callbacks.
3) Define two endpoints. '/api/1.0' and '/api/1.1'
4) In the 1.0 endpoint, enable the 'foo 1.0' resource, and alias it to foo.
5) In the 1.1 endpoint, enable the 'foo 1.1' resource, and alias it to foo.
6) Profit

This is probably worth getting detailed documentation about into the handbook. Let us know if you have problems.

boombatower’s picture

Assigned: Unassigned » boombatower

That makes sense. I think I'll set it up with the active endpoint/api in the main module and a historical module for the backwards compatible endpoints.

I should be implementing this in the next week or so and I will report back how it turned out.

boombatower’s picture

Seems to work fairly well. I tried to setup a nice clean way of storing the historical api sets of resources and endpoints in a separate module. It might be something that services could support either as a sub module or maybe I should make a separate project.

Still not sure whether it is best to store full copies of the resources and endpoints or just make "diffs" like update functions.

Example usecase:
1. Release api as 1.0.
2. Change the signature of a method and release as 1.1.
3. Create historical module to provide 1.0.
4. New API release 1.2.
5. Historical 1.1.

The historical module then could either contain two sets of copies of resources and endpoints (4 total) or simply a copy of the 1.0 that it uses as a basis. The 1.0 resources and endpoints would simply override the necessary information and provide wrapper methods that call the current API. When 1.1 becomes historical those wrappers may need to be updated to point to 1.1 historical which points to 1.2 (current) API. The 1.1 resource and endpoint definitions would then start with 1.0 as a basis and modify again what is needed.

Just trying to make sure this is setup is such a way that it is easy to maintain. So lets look at a detailed example (obviously more can change then signature, but should follow same rules).

Base services API:
-- 1.0 --
api/foo(int, string)
api/bar(struct)

Changes (per release):
-- 1.1 --
api/foo(string, string)
api/bar(struct)

-- 1.2 --
api/foo(string, string)
api/bar(struct)
api/rawr()

-- 1.3 --
api/foo(string, string)
api/bar(struct)
api/rawr(int)

-- 1.4 --
api/foo(string, string)
api/bar(struct)
api/rawr()

The historical module will then alter the base module endpoint to make it whatever the most current version number is (which should be a constant or calculated value in historical module). The historical module then needs to provide 1.0 - 1.3.

-- 1.0 --
So the 1.0 structure is exact copy of what was in 1.0 release except that foo points to foo_1_0() and bar is left alone (aka points to current) since 1.0 differs from current in that it's foo has a different signature.

function foo_1_0($int, $string) {
  return foo(convert_to_word($int), $string);
}

-- 1.1 --
Points to all current, just missing rawr() like 1.0.

-- 1.2 --
Points to all current.

-- 1.3 --
rawr points to rawr_1_3().

function rawr_1_3($int) {
  return rawr();
}

-- 1.4 --
Not present in historical module since base module provides it as active api. The historical module just sets it as 1.4.

/**
 * Implements hook_default_services_endpoint_alter().
 */
function conduit_api_default_services_endpoint_alter(&$endpoints) {
   $endpoints['base_module']->path = 'api/1.4';
}

If you have more complex setups you may need to update something like rawr_1_3($int) to call rawr_1_4() which then points to current (1.5, rawr()).

I think this makes more sense then having straight copies of the resources and endpoints and then I guess just tweaking the methods in them? and/or having entire copies of the methods? That seems to have little flexibility if you were to change the code inside those methods it would be a mess. I think stepping it up like this resembles updates system and makes sense.

So I ended up creating some helper functions that convert a previous version resource/endpoint to the version specified so you can setup a bunch of "diff" functions that start out by call previous function (or base) and then running the helper function to update name, title, version, array keys ect then overriding anything that is necessary. So the historical module simply needs to implement the services hooks and then auto call all these diff functions to generate a set of historical resources/endpoints.

I get the feeling that although this is a good practice (like writing update hooks) not everyone will do this so probably just start a new project to maintain a minimal sub-services API that provides the helper functions and update_N() style hooks for each version the historical module provides.

Thoughts?

boombatower’s picture

Title: How can one version the methods to provide backwards compatability » Provide historical API to simplify providing backwards compatability
Category: support » feature

Created temporary sandbox project to build an API to make historical services APIs simpler http://drupal.org/sandbox/boombatower/1117838.

I'll post patches here.

boombatower’s picture

Status: Active » Needs review
StatusFileSize
new13.67 KB

Test locally and seems to work well.

http://i.imgur.com/0rrqD.png
The latest shows up on top since the key is just conduit and the others are all suffixed with the version. I figured that is best since it is the only one you should modify normally and if any integration modules rely on the key they won't be broken when the historical information is added.

voxpelli’s picture

Subscribe

boombatower’s picture

For example, in my not yet released module I implement the following so that the current/initial api shows up as 1.0 when my conduit_historical module is enabled.

/**
 * Implements hook_services_historical_info().
 */
function conduit_historical_services_historical_info() {
  return array(
    'conduit' => array(
      'endpoint' => 'conduit',
      'version' => '1.0',
    ),
  );
}

/**
 * Implements hook_services_historical_endpoint().
 */
function conduit_historical_services_historical_endpoint() {
  // @TODO Temporary until a new version is created.
  $endpoints = conduit_default_services_endpoint();
  return $endpoints['conduit'];
}

Then it shows up as 1.0 so clients can start using as 1.0. Once I release a 1.1 I simply copy the 1.0 (from vcs) into the base endpoint hook and create a 1_0 update hook with the 1.0 resources and update the info() hook version to 1.1. (you may also need to make wrapper callbacks depending on the changes)

Next time I just add a update_1_1() and make the "diff" changes.

I spent a few hours going over a number of possibilities and I think this hits the most cases while making it quite simple to use, anyone with a more complex case can simply use hook_services_..._alter() to change the endpoints or resources further since they will all have nice keys. In my example the conduit endpoint would be conduit_1_0, conduit_1_1 and the resources will be the same with the version suffixed. (suppose I could add another drupal_alter() for endpoints and resources with the $version passed)

Side note, in my default install profile I then change the paths (which are defined to be conduit, conduit/1.0, etc) to api/... as follows.

/**
 * Implements hook_default_services_endpoint_alter().
 */
function default_default_services_endpoint_alter(&$endpoints) {
  foreach ($endpoints as &$endpoint) {
    if (strpos($endpoint->name, 'conduit') === 0) {
      $endpoint->path = str_replace('conduit', 'api', $endpoint->path);
    }
  }
}

I think that creates a really slick setup that is quite configurable based on which modules (or profile) are enabled (three layers: conduit, conduit_historical, profile).

boombatower’s picture

Lastly, make sure you read through .api.php for details.

boombatower’s picture

StatusFileSize
new14.73 KB

Added a few extra lines of documentation.

boombatower’s picture

StatusFileSize
new10.93 KB

Straight diff one can use with drush make.

pcarman’s picture

subscribing

boombatower’s picture

Project: Services »
Version: 7.x-3.x-dev »
Status: Needs review » Fixed

It was decided to maintained as separate project for now. Since the code is functional I'm marking this as fixed, but I'll fix up a few typos and what not before making a release.

boombatower’s picture

Project: » Services Tools
Version: » 7.x-3.x-dev

Decided to include a few tools like this in one project.

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.