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?