Anterofit Release Announcement ================ Anterofit has been a long time coming. I've been conceptualizing it ever since I started tinkering with Rust, something like over three years ago. For the longest time, I thought it wasn't possible to have it do everything I wanted without compiler plugins, which dampened my enthusiasm somewhat as they are perpetually unstable and break occasionally without much warning. I discovered this while working on [a previous attempt at the same concept][ferrite], which I had all but completely forgotten about before starting from scratch on Anterofit. As it turns out, I was partially right, as the macro-based implementation has one big, ugly limitation, but the future is looking bright. [ferrite]: https://github.com/abonander/ferrite Inspiration ----------- I basically straight-up copied the concept behind [Retrofit][retrofit], a Java library I have been using for many years. It has great utility in Android development, where there is often a want for an app to talk to a server backend to store user data and things of that nature. The basic idea is that you write a Java interface describing a REST API (which can, and should, be split across multiple interfaces), using annotations to add the necessary information to construct a request, and then Retrofit constructs an instance of that interface such that calling methods on it issues requests and returns their responses. Serialization is supported in both directions, so you can set any serializable object as the request body and have the response be deserialized to some POD object. For a useful example, the interface and usage to fetch a post from the [JSONPlaceholder API][jsonplaceholder], which has been immensely useful in developing and testing Anterofit, would look something like this: ```java public class Post { public long userId; public long id; public String title; public String body; } // Calling it a "service" comes from Retrofit's only example on interface declaration // I've used the same convention ever since public interface PostService { @POST("posts") Call createPost(@Field("userid") long userId, @Field("title") String title, @Field("body") String Body); @GET("posts/{id}") Call getPost(@Path("id") long id); } ``` And then to use it, you construct an instance of the `Retrofit` class and have it create an object from the interface class and call the declared methods on it: ```java Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://jsonplaceholder.typicode.com/") // You actually need to supply a converter to parse JSON responses into objects // That would be done before calling `.build()` .build(); PostService service = retrofit.create(PostService.class); Call newPost = service.createPost(42, "Hello, world!", "Lorem ipsum dolor sit amet"); // The JSONPlaceholder API doesn't save anything for obvious reasons; // this will return a Post instance with some filler text instead. Call post = service.getPost(0); ``` One shortcoming of Retrofit's documentation is that it doesn't show how to execute the `Call` instance to access the response. In the version of Retrofit that I'm used to using (v1.x), you either add an extra parameter for a callback instance and declare the return type `void`, or use the return type directly (which makes the request synchronous). This makes things a little more clear, but if you want an asynchronous and a synchronous call to the same endpoint, you need to declare two different methods, which is kind of ugly. Anterofit functions similarly to how Retrofit does now, but hopefully the documentation is sufficiently detailed to avoid confusion. [retrofit]: http://square.github.io/retrofit/ [jsonplaceholder]: http://jsonplaceholder.typicode.com Introducing Anterofit --------------------- Anterofit is a collection of macros that makes it easy to wrap REST APIs using Rust traits. Superficially, it functions similarly to Retrofit, but instead of constructing trait instances at runtime (which would be hacky and full of overhead), Anterofit simply rewrites the trait declarations and injects all the necessary details for making a request and parsing the response. This makes the implementation much less magical, more approachable and more maintainable. Also, much easier to document. Additionally, Anterofit is futures-aware (`Call` implements `Future`), so that you can poll for the status of requests from an event loop. However, for standalone use, it also provides inherent methods equivalent to `Future::poll()` and `Future::wait()` without requiring use of the `futures` crate. I have great plans for futures integration; see the [Looking to the Future / Tokio](#tokio) section for details. The name didn't really sound "cool" when I first came up with it, but it's grown on me. Retrofit is an appropriate name for a framework which does all the setup at runtime, so I figured that since Anterofit does almost all the setup at compile time instead (in true Rust fashion), I should try to find (or construct) the antonym of "retrofit". I would like to say I coined "anterofit" out of my own brilliance, by realizing the opposite of "retro-" could be considered to be "antero-", but I didn't. I stole the word from [this StackExchange answer](http://english.stackexchange.com/a/150352) instead. Technically, doesn't the word exist now that there's a semi-legitimate usage for it? How It Works ------------ I have written a [User's Guide](GUIDE.md) going into much more detail, but here's the basics: * Use the `service!{}` macro to write a trait interface describing a REST API endpoint (for example, the equivalent of the above example for Retrofit): ```rust #[macro_use] extern crate anterofit; // Serde is also supported extern crate rustc_serialize; #[derive(Debug, RustcDecodable)] struct Post { pub userid: Option, pub id: u64, pub title: String, pub body: String } service! { pub trait PostService { /// Get a Post by id. fn get_post(&self, id: u64) -> Post { // Using normal Rust expressions was easier to implement than trying to emulate // Retrofit's annotation approach with attributes; // Plus, it's more elegant, IMO. GET("/posts/{}", id) } /// Create a new Post under the given user ID with the given title and body. fn create_post(&self, userid: u64, title: &str, body: &str) -> Post { POST("/posts/"); // Provide HTTP form fields as key-value pairs fields! { "userId" => userid, // Shorthand for when the identifier is the same as the key title, body } } } } ``` * Construct an adapter with the base URL and serializers (unlike Retrofit, JSON serialization is provided out-of-the-box): ```rust use anterofit::{Adapter, Url}; let url = Url::parse("https://jsonplaceholder.typicode.com").unwrap(); let adapter = Adapter::builder() .base_url(url) // Shorthand for setting both JSON serializer and deserializer .serialize_json() .build(); ``` * Then call trait methods on the adapter, or coerce it to a trait object: ```rust // Instead of having the adapter create opaque implementations of service traits like Retrofit, // all service traits are implemented for `anterofit::Adapter` by default; // using generics like this helps keep service methods namespaced. fn create_post(post_service: &T) { let post = post_service.new_post(42, "Hello, world!", "Lorem ipsum dolor sit amet") // Requests in Anterofit aren't sent until executed: // by default, the request will be executed via blocking I/O on a background thread. // This returns `Call` which can be polled on for the result. .exec() // Equivalent to `Future::wait()` without using types from `futures` .block() .unwrap(); println!("{:?}", post); } // `anterofit::Adapter` is object-safe! fn fetch_posts(post_service: &PostService) { let posts = post_service.get_posts() // Shorthand for .exec().block(), but executes the request on the current thread. .exec_here() .unwrap(); for post in posts.into_iter().take(3) { println!("{:?}", post); } } create_post(&adapter); fetch_posts(&adapter); ``` That's it. No magic, no overly complicated metaprogramming, less work being done both at compile time *and* at runtime. On top of that, Anterofit is an order of magnitude more type-safe than Retrofit is. The `service!{}` macro is a bit of a mess mostly because of the limitations of `macro_rules!` macros, but I have plans to replace this using procedural macros. See the [Looking to the Future / Procedural Macros](#procedural-macros) section for more details. Comparison to Retrofit ---------------------- This is kind of apples-and-oranges given that Java and Rust are two very different platforms, but I figured the "order of magnitude more type-safe" claim in the previous section needed substantiating. * In Retrofit, if you pass the wrong class to `Retrofit.create()`, you get a runtime exception. Meainwhile in Anterofit, if you try to coerce `anterofit::Adapter` to a type that isn't a service trait, you get a compiler error pointing right to the problem, and suggestions for types that would work. * Serialization in Retrofit relies on a lot of magic (i.e. class metadata at runtime). Serialization for classes doesn't have to be explicitly implemented (at least if using the GSON converter), so if you use the wrong type you might get surprising results. In Anterofit, serialization for a given type has to be explicitly implemented; you'll get a compiler error otherwise. * In Retrofit, missing fields in deserialized objects are initialized to their defaults as per the Java language spec, so `null` for reference types and `0` or `false` for primitives. In Anterofit, missing fields that are not `Option` are a serialization error. Though, to be honest, this depends on how deserialization is implemented. Easily Construct API Wrappers ----------------------------- The design of Anterofit doesn't just take end-user applications into account. If you're using Anterofit to create an API wrapper for some popular REST API, say, Reddit's or Github's, exposing the particular implementation details of your wrapper is probably undesirable. For example, you might want to use the type system to restrict access to some APIs in certain states, such as statically preventing errors in trying to access the user's private data before they've logged in. And you probably want to make sure that only, e.g., JSON serialization is used for both request and response, to avoid confusing errors with data formats. In these and similar cases, you'll probably want to still expose your service trait definitions as part of your API because you'd have to repeat most of their definitions elsewhere, but you probably don't want them to be used with any old `anterofit::Adapter` instance, especially if you want to prevent people who are using multiple wrappers at once from shooting themselves in the foot by using an adapter set to the wrong endpoint for some service trait. For this case, Anterofit provides a way to suppress the default implementation for `anterofit::Adapter` and provide one or more alternate implementations, which I've chosen to call "delegates". Delegates are more like shells really, as they're expected to provide an accessor for an instance of `anterofit::Adapter`, but this can be entirely opaque; you can totally use a private field or method, as long as it obeys Rust's visibility rules. For example, a delegate implementation for `PostService`: ```rust pub struct MyDelegate { adapter: ::anterofit::Adapter, } service! { pub trait PostService { // Methods omitted for brevity } // The expression inside the braces is expected to be `FnOnce(&Self) -> &Adapter<...>` impl for MyDelegate { |this| &this.adapter } } let delegate: MyDelegate = ...; create_post(&delegate); fetch_posts(&delegate); ``` Now, only `MyDelegate` implements `PostService`, making abstraction possible while keeping noise to a minimum. Unanswered Design Questions --------------------------- I've put a lot of thought into the design of Anterofit, and I've tried to anticipate the most common and the most interesting use-cases, but there's some parts I'm still not quite sure about and I'm interested in feedback. I've listed some of my doubts on [the first Github issue on the repo](https://github.com/abonander/anterofit/issues/1), please feel free to share your opinions there, or suggest other areas which might be improved. Discussion on the announcement post is also welcome. I will add any non-negligible concerns to the Github issue for archiving and further discussion. Looking to the Future --------------------- ### Tokio Once the dust around Hyper's transition to async I/O with [Tokio][tokio] settles, I plan on branching [`multipart`][multipart] to support futures, and then rebuilding Anterofit on top of it. The public API won't change much, save for a couple new executors which are aware of Tokio event loops; one to execute all requests on a background thread's event loop, and one to execute requests on the event loop running in the current thread, making Anterofit truly asynchronous. [tokio]: https://github.com/tokio-rs/tokio [multipart]: https://github.com/abonander/multipart ### Procedural Macros The current implementation of the `service!{}` macro is mostly satisfactory, but it's rather noisy due to having to support different combinations of syntax features, mainly generics and `where` clauses, and even then it requires using `[]` as delimiters for generics than `<>` because angle brackets aren't supported as token-tree delimiters. In reality, none of this is relevant to the actual implementation of service trait generation; other than separating method signatures from their bodies and rewriting the return type, the `service!{}` macro doesn't really care about the specifics. With the introduction of [procedural macros beyond custom derive][proc-macro], the solution was clear: implement `service!{}` as an attribute. I was so excited, I wanted to immediately try prototyping it. The going was pretty easy, and I had a mostly finished prototype, but when I went to test it, I discovered a core problem with my prototype: attribute procedural macros *weren't implemented in the compiler yet!* That wouldn't do, so I decided I'd [go ahead and get that out of the way][attr-pr] (with @jseyfried as an excellent mentor). Once attribute procedural macros were working and I had my prototype up to speed, I found that the implementation was far more elegant, extensible, easier to follow, and almost shorter than the implementation in macros was! And the best part: it didn't have any of the syntactic limitations as the macro version. I was immensely excited. With the `#[service]` attribute, the declaration of `PostService` would look like this: ```rust #[service] pub trait PostService { fn get_post(&self, id: u64) -> Post { // Since this is already valid Rust syntax, it doesn't break the parser GET("/posts/{}", id) } fn create_post(&self, userid: u64, title: &str, body: &str) -> Post { POST("/posts/"); fields! { "userId" => userid, title, body } } } ``` This doesn't look too much different, but the main point is that when implemented this way, generics and `where` clauses aren't a problem. They can simply be passed on after parsing and re-emitted. Superficially, it also saves a level of indent which helps limit right-drift. The main feature is the much simpler implementation, which also has, IMO, better diagnostics for unsupported features (like associated types and unsafe traits, which can be implemented but don't seem relevant currently). Unfortunately, it looks like the remaining procedural macro features won't be fast-tracked to stabilization like custom derive was. I'm ambivalent about the reasoning; ideally, `TokenStream` should have a lot more flexible API and support things like hygiene before we start encouraging people to use procedural macros *en masse*. However, since my implementation works as-is and, with the possible exception of hygiene, it seems like these things can be implemented backwards-compatibly, I don't think there's much reason to keep procedural macros themselves unstable for very long. [proc-macro]: https://github.com/rust-lang/rfcs/pull/1566 [attr-pr]: https://github.com/rust-lang/rust/pull/38842 ### *In-Vivo* Test Of course, no great project is complete without a sister project built with it as an *in-vivo* test and an endless well of design feedback; Rust has Servo, LLVM has Clang... okay, I've run out of examples, but you get my point. As counterpart to Anterofit, I plan on reviving my [Reddit API Wrapper for Rust][rawr] project, which I initially started building Ferrite for. The eventual goal is to have RAWR testing as many features of Anterofit as possible; interceptors, delegation, procedural macros, and maybe an auxilliary Imgur API for uploading images, to test file uploads. I may wait for this until I get Anterofit ported to Hyper-Tokio, however. [rawr]: https://github.com/abonander/rawr Conclusion ---------- I don't really have a big conclusion statement put together, it just seemed weird to end this announcement without saying something. I could have kept polishing this perpetually in private but I figured that wouldn't do much good for me, the library, or anyone else. So please try it and let me know what you think. I'd also love to hear about any potential use cases that aren't covered by the current API.