1

We are trying to write a Java API library to wrap the Canvas LMS REST API. We have a reader and a writer interface for each type of object that the API deals with. For example we have a UserReader interface that will return User objects from the "List users in course" endpoint as well as "Search users in consortium" endpoint.

The question we are struggling with is how best to implement all the optional parameters to API calls. If you look at the specifications for the two linked endpoints, you will see that while they both return User objects, they take very different parameters. The initial implementation just added a bunch of lists (which can be empty) and Optional arguments (which can be empty) to the listUsersInCourse method. So a call to this method if you don't want to specify any of the optional parameters would look like the following. And yes, you could have a no-argument convenience version of the method for not specifying any options but as soon as you want to use any of the options, you'll have to deal with them all anyway.

List<User> userList = userReader.listUsersInCourse("courseId", 
    Collections.emptyList(), Optional.empty(), Collections.emptyList());

This is a terrible idea because 1) some endpoints have a lot of options which makes for ridiculous method signatures and 2) if Canvas adds or changes options (which has already happened) it will break our method signature and force everyone depending on our library to update their code.

Optional, named parameters would be nice but Java doesn't do this. I have looked at some other API libraries and it seems somewhat rare to have this many parameters for use in a REST API. I have thought about a few ways to address this but I'm not sure what the best option is. Here is what I've come up with so far:

1

A relatively structured way to do this would be to follow a builder pattern with methods to add specific options. So for example:

List<User> userList = userReader.withSearchTerm("Myname")
          .withEnrollmentType(EnrollmentType.Student)
          .withEnrollmentType(EnrollmentTye.Teacher)
          .listUsersInCourse("courseId");

This way if new options get added, it's just a new method to add and doesn't break existing code. The downside is that not all option methods in a given reader class would apply to all of the API methods in it which could be confusing. For example both of the user list methods linked above take a search term but the consortium one doesn't accept enrollment type. So you would always have to double check the Canvas API documentation to see what options are valid for a given call. Also if an option is added, we would have to release a new version of the library before people could access it. The Canvas API is under active development so things are definitely subject to change.

2

A potentially even more structured way to go would be to create call-specific options classes to constrain things even more. This would look like so:

CourseUserListOptions opts = new CourseUserListOptions();
opts.addSearchTerm("Myname");
opts.addEnrollmentType(EnrollmentType.Student);
opts.addEnrollmentType(EnrollmentType.Teacher);
List<User> userList = userReader.listUsersInCourse("CourseId", opts);

There would be another ConsortiumUserSearchOptions class that wouldn't have an addEnrollmentType method. While this is probably the most type safe and well-defined way of doing it, it seems like this would be a nightmare to write and maintain.

3

Another approach would be to have a more generic addOption(String key, String value) method. The above call would turn into:

userReader.addOption("search_term", "Myname");
userReader.addOption("enrollment_type[]", EnrollmentType.Student.name());
userReader.addOption("enrollment_type[]", EnrollmentType.Teacher.name());
List<User> userList = userReader.listUsersInCourse("CourseId");

This would be significantly less effort to code and maintain on our side but forces the user of the library to know a little bit more and pass in magic strings that are defined in the Canvas API. As noted in #1 above, the biggest benefit of being less structured is that when the Canvas API adds new options, our users could immediately make use of it rather than having to wait for a new release of the library that has the new method included.

4

One more option could also just accept a completely generic list of options in every API method and force the user of the library to be more involved in constructing the request. This would look something like this:

List<Pair> optsList = new ArrayList<>();
optsList.add(new ImmutablePair("search_term", "Myname"));
optsList.add(new ImmutablePair("enrollment_type[]", EnrollmentType.Student.name());
optsList.add(new ImmutablePair("enrollment_type[]", "EnrollmentType.Teacher.name());
List<User> userList = userReader.listUsersInCourse("courseId", optsList);

Those are the options I have come up with. Which one would you find most useful if you were using this API library? Which one would you rather write/maintain? Am I completely missing a better way of doing it?

ToeBee
  • 109
  • 9
  • 1
    I'm not sure this is a good fit for StackOverFlow which is for asking assistance on specific programming problems. Your question will have many opinion based answered and I think you should move it over the http://programmers.stackexchange.com which may get you better results. – drekka Jun 22 '16 at 00:56
  • I would probably opt for implementing both options 2 and 3. I don't see how option 2 would be considerably harder than option 1. – jxh Jun 22 '16 at 00:59
  • @jxh it is harder because the Canvas API has a LOT of endpoints so we would be creating tens, maybe even a hundred endpoint-specific options classes. Here is the full listing of the API: https://canvas.instructure.com/doc/api/all_resources.html – ToeBee Jun 22 '16 at 01:06
  • What I mean is that your API should enforce valid parameters anyway, even with option 1. If you think it is easier to code the enforcement in a static table to check the generic list or parameters against, then you can do that. But with either option 1 or 2, I would still also include option 3. – jxh Jun 22 '16 at 01:13
  • Even though it's kind of a pain, I agree that #2 is the best for users - it takes advantage of Java's strong types to help users understand which options are available for which call. It's very reminiscent of [Amazon's AWS Java API][https://aws.amazon.com/sdk-for-java/], where there's a fluent `CreateBucketRequest` class for the `AmazonS3Client::createBucket` method. As a compromise to your bleeding-edge customers, I'd include #3 as well. – Andrew Rueckert Jun 22 '16 at 01:19
  • @AndrewRueckert thanks for mentioning the AWS API. It wasn't one I had looked at but it does indeed seem to have a similar construct. – ToeBee Jun 22 '16 at 01:29

2 Answers2

0

If I'm understanding you correctly, your question is along the lines of, "how much work should I consider doing to help my users"

It sounds like the Canvas API is complicated and still in flux, which, would make me lean towards pushing the burden onto the users, such as your 4th option. This still leaves open the possibility of implementing your 2nd option on top of that if certain pieces are too complicated for the users to use.

thepaulpage
  • 4,614
  • 2
  • 25
  • 39
0

You can also create a JAX-RS library using the client proxy.

Define an interface like

@Path("/api")
public interface CanvasAPI {

@GET
@Path("/v1/accounts/{accountId}/courses")
Response getCoursesForAccount(@PathParam("accountId") long accountId,
    @QueryParam("with_enrollments") boolean withEnrollments, @QueryParam("enrollment_term_id") long termId);

}

Then create a client proxy

new ResteasyClientBuilder().build().target("<base_url>").proxy(CanvasAPI.class);

This gives you a rest proxy class that you can use like a normal class

CanvasAPI api = new ResteasyClientBuilder().build().target("<base_url>").proxy(CanvasAPI.class);
data = api.getCoursesForAccount(5l, true, 2l); 

This method is pretty easy to keep insync with the api documentaion because you just have to mimic the calls defined in the spec and let the proxy build all of the inner workings for you.

pfranza
  • 3,292
  • 2
  • 21
  • 34