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.
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
}
});
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);
}];
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
});
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.
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);
}];
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) // ??