Skip to content

Instantly share code, notes, and snippets.

@cvasilak
Last active December 15, 2015 23:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save cvasilak/4a5f51b8d1ad9cc7f21b to your computer and use it in GitHub Desktop.
Save cvasilak/4a5f51b8d1ad9cc7f21b to your computer and use it in GitHub Desktop.
AeroGear - Nested Resources Support

Nested Resources Support

This document describes the different approaches discussed in the mailing list regarding nested resource support in the AeroGear library and specifically support in the Pipeline mechanism. Some appraches require no modifications and use the existing mechanism to support the nested resource while some others require modifications as well as they introduce new API constructs (e.g. SubPipe, Junction, etc) to the Pipeline.

Code examples for each approach are presented that go against an imaginative soccer RESTfull API that has the following structure:

http://soccer.org/leagues/{id}/teams/{id}/players/{id}

NOTE: the names given to methods and params are not final and just illustrate the concept.

Use of the existing Pipeline API

Approach #1 : Enchanced read

The basic idea here is to enchance the read function with an extra parameter resource that the user can pass in the nested resource path that he is interested in.

iOS Example

NSURL* serverURL = [NSURL URLWithString:@"http://soccer.org/"];

AGPipeline* pipeline = [AGPipeline pipelineWithBaseURL:serverURL];

// Add a REST pipe for the 'leagues' endpoint
id<AGPipe> leaguesPipe = [pipeline pipe:^(id<AGPipeConfig> config) {
    [config setName:@"leagues"];
}];

// READ "teams" for league "seattle"

[leaguesPipe read:@"seattle" resource:@"/teams" success:^(id responseObject) {
    // LOG the JSON response, returned from the server:
    NSLog(@"READ RESPONSE\n%@", [responseObject description]);
} failure:^(NSError *error) {
    // when an error occurs... at least log it to the console..
    NSLog(@"Read: An error occured! \n%@", error);
}];

// READ "teams" for league "new_york"

[leaguesPipe read:@"new_york" resource:@"/teams" success:^(id responseObject) {
    // LOG the JSON response, returned from the server:
    NSLog(@"READ RESPONSE\n%@", [responseObject description]);
} failure:^(NSError *error) {
    // when an error occurs... at least log it to the console..
    NSLog(@"Read: An error occured! \n%@", error);
}];

// READ for team "trebuchet" in league "seattle"

[leaguesPipe read:@"seattle" resource:@"/teams/trebuchet" success:^(id responseObject) {
    // LOG the JSON response, returned from the server:
    NSLog(@"READ RESPONSE\n%@", [responseObject description]);
} failure:^(NSError *error) {
    // when an error occurs... at least log it to the console..
    NSLog(@"Read: An error occured! \n%@", error);
}];

// READ "players" for team "trebuchet" in league "seattle"

[leaguesPipe read:@"seattle" resource:@"/teams/trebuchet/players" success:^(id responseObject) {
    // LOG the JSON response, returned from the server:
    NSLog(@"READ RESPONSE\n%@", [responseObject description]);
} failure:^(NSError *error) {
    // when an error occurs... at least log it to the console..
    NSLog(@"Read: An error occured! \n%@", error);
}];

// READ player with Id 1 for team "trebuchet" in league "seattle"

[leaguesPipe read:@"seattle" resource:@"/teams/trebuchet/players/1" success:^(id responseObject) {
    // LOG the JSON response, returned from the server:
    NSLog(@"READ RESPONSE\n%@", [responseObject description]);
} failure:^(NSError *error) {
    // when an error occurs... at least log it to the console..
    NSLog(@"Read: An error occured! \n%@", error);
}];

JavaScript Example

var leaguePipe = AeroGear.Pipeline();
pipeline.add( "leagues", {
  baseURL: "http://soccer.org/",
}).leagues;

//READ "teams" for league "seattle"
leaguePipe.read({
  id:       "seattle",
  resource: "/teams",
  success:  function( data ){
     //do stuff
  }
});

//READ "teams" for league "new_york"
leaguePipe.read({
  id:       "new_york",
  resource: "/teams",
  success: function( data ){
     //do stuff
  }
});

//READ for team "trebuchet" in league "seattle"
leaguePipe.read({
  id:       "seattle",
  resource: "/teams/trebuchet",
  success: function( data ){
     //do stuff
  }
});

//READ "players" for team "trebuchet" in league "seattle"
leaguePipe.read({
  id:       "seattle",
  resource: "/teams/trebuchet/players",
  success: function( data ){
     //do stuff
  }
});

//READ player with Id 1 for team "trebuchet" in league "seattle"
leaguePipe.read({
  id:       "seattle",
  resource: "/teams/trebuchet/players/1",
  success: function( data ){
     //do stuff
  }
});

Approach #2 : use of baseURL

The basic idea here is to use the existing configuration option baseURL to setup the pipe's parent path. For example:

AGPipeline* pipeline = [AGPipeline pipeline];

id<AGPipe> teamsForLeagueSeattle = [pipeline pipe:^(id<AGPipeConfig> config) {
    [config setBaseURL:[NSURL URLWithString:@"http://server.org/leagues/seattle"]];
    [config setName:@"teams"];
}];

// READ for team "trebuchet" (implies parent path as set in the baseURL in the pipe config)

[teamsForLeagueSeattle read:@"trebuchet" success:^(id responseObject) {
    // LOG the JSON response, returned from the server:
    NSLog(@"READ RESPONSE\n%@", [responseObject description]);
} failure:^(NSError *error) {
    // when an error occurs... at least log it to the console..
    NSLog(@"Read: An error occured! \n%@", error);
}];

// READ player with Id 1 for team "trebuchet" in league "seattle" (implies parent path as set in the baseURL in the pipe config)

id<AGPipe> trebuchetPlayersPipe = [pipeline pipe:^(id<AGPipeConfig> config) {
    [config setBaseURL:[NSURL URLWithString:@"http://server.org/leagues/seattle/teams/trebuchet"]];
    [config setName:@"players"];
}];

[trebuchetPlayersPipe read:@"1" success:^(id responseObject) {
    // LOG the JSON response, returned from the server:
    NSLog(@"READ RESPONSE\n%@", [responseObject description]);
} failure:^(NSError *error) {
    // when an error occurs... at least log it to the console..
    NSLog(@"Read: An error occured! \n%@", error);
}];

Approach #3 : add a config option parentPath in PipeConfig

The basic idea here is to add an additional configuration option parentPath to setup the pipe's parent path. Similar to the approach#2 but with one important difference. Wildcards are used in place of static id's, which are dynamically expanded by the read method. This allows the reuse of the pipe for different data. For example:

class League {
  @RecordId
  String name;
  
  List<Team> teams;
}

class Team {
  @RecordId
  String name;
  
  List<Player> currentPlayers;
  
}

class Player {
  @RecordId
  String name;
  
  Team currentTeam;
  
  List<Team> teamsPlayedFor;
}

class SoccerApplication {

//...
URL baseURL = new URL("http://server.org");

Pipeline pipeline = new Pipeline(baseURL);

// Add a REST pipe for the 'leagues' endpoint
PipeConfig leaguesConfig = new PipeConfig(baseURL, League.class);
leaguesConfig.setName("leagues");

Pipe leagues = pipeline.pipe(Leagues.class, leaguesConfig);
pipe.read(new Callback<List<Leagues>>() {
        //...success/failure
});

// READ "teams" for league "seattle"

PipeConfig teamsConfig = new PipeConfig(baseURL, Team.class);
teamsConfig.setName("teams");
teamsConfig.setParentPath("/leagues/{id}/");

Pipe teams = pipeline.pipe(Teams.class, teamsConfig);

// assume 'league' object has the id 'seattle'
teams.read(league, new Callback<List<Team>>() {
        //...success/failure
});

// READ for team "trebuchet" in league "seattle"

// assume 'league' object has the id 'seattle'
// assume 'team' object has the id 'trebuchet'
teams.read(league, team, new Callback<List<Team>>() {
        //...success/failure
});

// READ "players" for team "trebuchet" in league "seattle"

PipeConfig playersConfig = new PipeConfig(baseURL, Player.class);
playersConfig.setName("players");
playersConfig.setParentPath("/leagues/{id}/teams/{id}");

Pipe players = pipeline.pipe(Player.class, playersConfig);

players.read(league, team, new Callback<List<Player>>() {
        //...success/failure
});

// READ player with Id 1 for team "trebuchet" in league "seattle"
players.read("1", league, team, new Callback<List<Team>>() {
        //...success/failure
});

Modify the existing Pipeline API

The goal of all the approaches discussed below is that to treat the nested resources as full time citizens. That is, each nested resource is treated as Pipe and the usual CRUD methods apply to it as well.

Approach #4 : Introduce subPipe method on the Pipe.

The basic idea of this approach is to add an additional method subPipe on an existing Pipe that will point to the nested resource. The subpipe method will leverage the available information on the parent Pipe to build up the base url of the nested Pipe. Further, additional nested resources are build by chaising calls in the subPipe method in the parent Pipe objects. For example:

NSURL* serverURL = [NSURL URLWithString:@"http://soccer.org/"];

AGPipeline* pipeline = [AGPipeline pipelineWithBaseURL:serverURL];

// Add a REST pipe for the 'leagues' endpoint
id<AGPipe> leaguesPipe = [pipeline pipe:^(id<AGPipeConfig> config) {
    [config setName:@"leagues"];
}];

// READ "teams" for league "seattle"

id<AGPipe> teamsForLeagueSeattle = [leaguesPipe subPipe:@"teams" for:@"seattle"];

[teamsForLeagueSeattle read:^(id responseObject) {
    // LOG the JSON response, returned from the server:
    NSLog(@"READ RESPONSE\n%@", [responseObject description]);
} failure:^(NSError *error) {
    // when an error occurs... at least log it to the console..
    NSLog(@"Read: An error occured! \n%@", error);
}];

// READ for team "trebuchet" in league "seattle"

[teamsForLeagueSeattle read:@"trebuchet" success:^(id responseObject) {
    // LOG the JSON response, returned from the server:
    NSLog(@"READ RESPONSE\n%@", [responseObject description]);
} failure:^(NSError *error) {
    // when an error occurs... at least log it to the console..
    NSLog(@"Read: An error occured! \n%@", error);
}];

// READ "players" for team "trebuchet" in league "seattle"

id<AGPipe> playersForTeamTrebuchet = [teamsForLeagueSeattle subPipe:@"players" for:@"trebuchet"];

[playersForTeamTrebuchet read:^(id responseObject) {
    // LOG the JSON response, returned from the server:
    NSLog(@"READ RESPONSE\n%@", [responseObject description]);
} failure:^(NSError *error) {
    // when an error occurs... at least log it to the console..
    NSLog(@"Read: An error occured! \n%@", error);
}];

// READ player with Id 1 for team "trebuchet" in league "seattle"

[playersForTeamTrebuchet read:@"1" success:^(id responseObject) {
    // LOG the JSON response, returned from the server:
    NSLog(@"READ RESPONSE\n%@", [responseObject description]);
} failure:^(NSError *error) {
    // when an error occurs... at least log it to the console..
    NSLog(@"Read: An error occured! \n%@", error);
}];

Approach #5 : Introduce a new construct Junction

The basic idea of this approach is to introduce a new method in the pipeline called junction that will list the series of consecutive Pipes that make up the path to the nested resource. To fill up the ID's of the parent resources, an enchanced read function will take a list of Parent objects where we extract the ID's using the object's RecordId annotation [see model above]

class SoccerApplication {
// ...
// assume setup of Pipes with names 'leagues', 'teams', 'players'
// ..

// read 'leagues' endpoint

pipeline.pipe("leagues").read(callback)

// READ "teams" for league "seattle"

// assume 'league' object has the id 'seattle'
pipeline.junction("league", "teams").read(league, callback)


// READ for team "trebuchet" in league "seattle"

// assume 'league' object has the id 'seattle'
// assume 'team' object has the id 'trebuchet'
pipeline.junction("leagues", "teams").read(league, team, callback)

// READ "players" for team "trebuchet" in league "seattle"

// assume 'league' object has the id 'seattle'
// assume 'team' object has the id 'trebuchet'
pipeline.junction("leagues", "teams", "players").read(league, team, callback)

// READ player with Id 1 for team "trebuchet" in league "seattle"

// assume 'league' object has the id 'seattle'
// assume 'team' object has the id 'trebuchet'
pipeline.junction("leagues", "teams", "players").read(league, team, player, "1" callback) // ??
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment