Sunday, April 19, 2009

Need More Rest

Note: This is all brand new and to be considered experimental right now, but I thought I'd give you guys a heads-up:

ERXRest can do a lot of various things, but I was never happy with the API and the complexity of the interfaces you have to implement to use it. Anjo did some work with ERD2REST, which is cool, but I still have a bunch of cases where I just want something like what Rails offers -- a very easy way to map annotated URLs onto (direct) action methods. When you just want to quickly make a json interface to your app (especially CRUD interfaces), but with support for additional custom actions (which ERXRest doesn't address), the rails approach makes a lot of sense ... So here are some new API's to try to make that easier:

in your Application constructor:


ERXRouteRequestHandler routeRequestHandler = new ERXRouteRequestHandler();
routeRequestHandler.addDefaultRoutes(Reminder.ENTITY_NAME);
routeRequestHandler.addDefaultRoutes(Person.ENTITY_NAME);
ERXRouteRequestHandler.register(routeRequestHandler);

(you can also add custom routes -- i.e. "/people/{person:Person}/company/{company:Company}/{something:String}" -- but the one above defines all the default routes that rails supports ... check out the javadoc for ERXRestRequestHandler for more info).

then you can make a controller class:

public class PeopleController extends ERXRouteController {
public PeopleController(WORequest request) {
super(request);
}

public Person person() {
Person person = (Person) objects().objectForKey("person");
return person;
}

public ERXKeyFilter personFilter() {
ERXKeyFilter filter = ERXKeyFilter.filterWithAttributes();
filter.include(Person.PREFERENCE_GROUPS).includeAttributes();
return filter;
}

public WOActionResults createAction() throws Exception {
ERXKeyFilter personFilter = personFilter();
personFilter.include(Person.COMPANY);
Person person = (Person) create(Person.ENTITY_NAME, personFilter);
editingContext().saveChanges();
return response(personFilter(), person);
}

public WOActionResults updateAction() throws Exception {
Person person = person();
update(person, personFilter());
editingContext().saveChanges();
return response(personFilter(), person);
}

public WOActionResults showAction() {
Person person = person();
return response(personFilter(), person);
}

public WOActionResults indexAction() throws Exception {
NSArray people = Person.fetchPersons(editingContext(), null, Person.LAST_NAME.asc().then(Person.FIRST_NAME.asc()));
return response(personFilter(), editingContext(), Person.ENTITY_NAME, people);
}
}

This is basically a DirectAction class, but it has a bunch of tools to make it easy to do the rest creates, updates, and lookups ... there is no security by default (just like rails), so you have to enforce security however makes sense, but it handles xml, json, and plist requests and for the cases where you're just doing relatively straightforward things, this is way easier than the other ERXRest stuff, though you trade off some optimized fetching and automatic security filtering that ERXRest does in exchange for the simplicity.

The determination of what to include/exclude both in rendering and in parsing is done with ERXKeyFilters, which allow hierarchical definitions of include/exclude filters for keypaths (so you say things like: start with an "include all attributes" base, but exclude "password", then include the "tasks" relationship, but don't show any attributes for that (so you just get task ids)). The code for that would be:


ERXKeyFilter filter = ERXKeyFilter.filterWithAttributes();
filter.exclude(Person.PASSWORD);
filter.include(Person.TASKS);


As an example of actually hitting the API, btw:

curl -X PUT -d '{ lastName="SchragTestAlso"; preferenceGroups=( {id=1000012; name="SomeName2"; } ); }' http://vdoop.local:51915/cgi-bin/WebObjects/MDTask.woa/ra/person/1000008.plist

this updates the person with id 1000008 with plist format, sets the last name to "SchragTestAlso", and replaces their preference groups with a single existing preference group id 1000012 and also updates the name of that group to be "SomeName2".

curl -X PUT -d '{ lastName:"SchragTestAlso"; preferenceGroups:[ {id:1000012; name:"SomeName2"; } ]; }' http://vdoop.local:51915/cgi-bin/WebObjects/MDTask.woa/ra/person/1000008.json

is the same thing in JSON, and

curl -X PUT -d '<Person><lastName>SchragTestAlso</lastName><preferenceGroups><PreferenceGroup id="1000012"><name>SomeName2</name></PreferenceGroup></preferenceGroups></Person>' http://vdoop.local:51915/cgi-bin/WebObjects/MDTask.woa/ra/person/1000008.xml

is the same thing in XML ... notice that just by changing the extension it automagically does request and response filtering for you with the right parser/writer.