Minor test refactor, major README update

This commit is contained in:
Ben Bader 2016-01-05 19:11:26 -08:00
Родитель 41500f416d
Коммит 4f0f4cacb8
8 изменённых файлов: 295 добавлений и 22 удалений

Просмотреть файл

@ -1,6 +1,7 @@
0.2.0 (unreleased)
------------------
- Add CompactProtocol implementation
- Add integration test suite
- Add service-client code generation
- Add service-client runtime implementation

283
README.md
Просмотреть файл

@ -1,22 +1,20 @@
Thrifty
=======
"It's Thrift, but cheaper"
"Thrift, but cheaper"
[![Build Status](https://travis-ci.org/benjamin-bader/thrifty.svg?branch=master)](https://travis-ci.org/benjamin-bader/thrifty)
Thrifty is an implementation of the Apache Thrift software stack for Android.
Thrift is a widely-used cross-language service-defition software stack, with a nifty interface definition language
Thrift is a widely-used cross-language service-definition software stack, with a nifty interface definition language
from which to generate types and RPC implementations. Unfortunately for Android devs, the canonical implementation
generates very verbose and method-heavy Java code, in a manner that is not very Proguard-friendly.
Like Square's Wire project for Protocol Buffers, Thrifty does away with getters and setters (and is-set and
Like Square's Wire project for Protocol Buffers, Thrifty does away with getters and setters (and is-setters and
set-is-setters) in favor of public final fields. It maintains some core abstractions like Transport and Protocol, but
saves on methods by dispensing with Factories and only only generating code for the protocols you actually need.
Code generation for everything but services is complete. If you are not using services, Thrifty is usable today for
serializing and deserializing Thrifts. Services are coming as soon as runtime support is ironed out.
saves on methods by dispensing with Factories and server implementations and only only generating code for the
protocols you actually need.
### Usage
@ -52,6 +50,272 @@ The major differences are:
- Fields that are neither required nor optional (i.e. "default") are treated as optional; a struct with an unset default field may still be serialized.
- TupleProtocol is unsupported at present
## Guide To Thrift and Thriftiness
Thrift is a language-agnostic remote-procedure-call (RPC) definition toolkit. Services, along with a rich set of
structured data, are defined using the Thrift Interface Definition Language (IDL). This IDL is then compiled into
one or more target languages (e.g. Java), where it can be used as-is to invoke RPC methods on remote services.
Thrifty is an alternate implementation of Thrift targeted at Android usage. Its benefits over the standard Apache
implementation are its greatly reduced method count and its increased type-safety. By generating immutable classes
that are validated before construction, consuming code is guaranteed that
#### Interface Definition Language
The Thrift IDL is a simple and standardized way to define data, data structures, and services:
```thrift
// Let's call this example.thrift
namespace java com.foo.bar
struct Query {
1: required string text,
2: optional i64 resultsNewerThan
}
struct SearchResult {
1: required string url,
2: required list<string> keywords = [], // A list of keywords related to the result
3: required i64 lastUpdatedMillis // The time at which the result was last checked, in unix millis
}
service Google {
list<SearchResult> search(1: Query query)
}
```
For the authoritative source on Thrift IDL, [Thrift: The Missing Guide][https://diwakergupta.github.io/thrift-missing-guide/] is an excellent introduction.
#### Generating Code
Use `thrifty-compiler` to compile IDL into Java classes:
```bash
java -jar thrifty-compiler.jar --out=path/to/output example.thrift
```
The example file will result in the following files being generated:
path/to/output/
- com/foo/bar/
- Google.java
- GoogleClient.java
- Query.java
- SearchResult.java
The interesting files here are, of course, our domain objects `Query` and `SearchResult`.
The latter looks like this:
```java
package com.foo.bar;
import com.bendb.thrifty.Adapter;
import com.bendb.thrifty.StructBuilder;
import com.bendb.thrifty.TType;
import com.bendb.thrifty.ThriftField;
import com.bendb.thrifty.protocol.FieldMetadata;
import com.bendb.thrifty.protocol.ListMetadata;
import com.bendb.thrifty.protocol.Protocol;
import com.bendb.thrifty.util.ProtocolUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class SearchResult {
public static final Adapter<SearchResult, Builder> ADAPTER = new SearchResultAdapter();
@ThriftField(
fieldId = 1,
isRequired = true
)
public final String url;
/**
* A list of keywords related to the result
*/
@ThriftField(
fieldId = 2,
isRequired = true
)
public final List<String> keywords;
/**
* The time at which the result was last checked, in unix millis
*/
@ThriftField(
fieldId = 3,
isRequired = true
)
public final Long lastUpdatedMillis;
private SearchResult(Builder builder) {
this.url = builder.url;
this.keywords = Collections.unmodifiableList(builder.keywords);
this.lastUpdatedMillis = builder.lastUpdatedMillis;
}
@Override
public boolean equals(Object other) {
if (this == other) return true;
if (other == null) return false;
if (!(other instanceof SearchResult)) return false;
SearchResult that = (SearchResult) other;
return (this.url == that.url || this.url.equals(that.url))
&& (this.keywords == that.keywords || this.keywords.equals(that.keywords))
&& (this.lastUpdatedMillis == that.lastUpdatedMillis || this.lastUpdatedMillis.equals(that.lastUpdatedMillis));
}
@Override
public int hashCode() {
int code = 16777619;
code ^= this.url.hashCode();
code *= 0x811c9dc5;
code ^= this.keywords.hashCode();
code *= 0x811c9dc5;
code ^= this.lastUpdatedMillis.hashCode();
code *= 0x811c9dc5;
return code;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("SearchResult").append("{\n ");
sb.append("url=");
sb.append(this.url);
sb.append(",\n ");
sb.append("keywords=");
sb.append(this.keywords);
sb.append(",\n ");
sb.append("lastUpdatedMillis=");
sb.append(this.lastUpdatedMillis);
sb.append("\n}");
return sb.toString();
}
public static final class Builder implements StructBuilder<SearchResult> {
private String url;
/**
* A list of keywords related to the result
*/
private List<String> keywords;
/**
* The time at which the result was last checked, in unix millis
*/
private Long lastUpdatedMillis;
public Builder() {
this.keywords = new ArrayList<String>();
}
public Builder(SearchResult struct) {
this.url = struct.url;
this.keywords = struct.keywords;
this.lastUpdatedMillis = struct.lastUpdatedMillis;
}
public Builder url(String url) {
if (url == null) {
throw new NullPointerException("Required field 'url' cannot be null");
}
this.url = url;
return this;
}
public Builder keywords(List<String> keywords) {
if (keywords == null) {
throw new NullPointerException("Required field 'keywords' cannot be null");
}
this.keywords = keywords;
return this;
}
public Builder lastUpdatedMillis(Long lastUpdatedMillis) {
if (lastUpdatedMillis == null) {
throw new NullPointerException("Required field 'lastUpdatedMillis' cannot be null");
}
this.lastUpdatedMillis = lastUpdatedMillis;
return this;
}
@Override
public SearchResult build() {
if (this.url == null) {
throw new IllegalStateException("Required field 'url' is missing");
}
if (this.keywords == null) {
throw new IllegalStateException("Required field 'keywords' is missing");
}
if (this.lastUpdatedMillis == null) {
throw new IllegalStateException("Required field 'lastUpdatedMillis' is missing");
}
return new SearchResult(this);
}
@Override
public void reset() {
this.url = null;
this.keywords = new ArrayList<String>();
this.lastUpdatedMillis = null;
}
}
private static final class SearchResultAdapter implements Adapter<SearchResult, Builder> {
// Uninteresting but important serialization code
}
```
The struct itself is immutable and has a minimal number of methods. It can be constructed only
with the assistance of a nested `Builder`, which validates that all required fields are set. Finally, an
Adapter implementation (whose body is omitted here because it is long and mechanical) that handles reading and
writing `HttpResponse` structs to and from `Protocols`.
Finally and separately, note `WebService` and `WebServiceClient` - the former is an interface, and the latter is an autogenerated implementation.
You may notice the similarity to protobuf classes generated by Wire - this is intentional!
The design principles codified there - immutable data, build-time validation, preferring fields over methods,
separating data representation from serialization logic - lead to better, safer code, and more breathing room
for Android applications.
#### Using Generated Code
Given the example above, the code to invoke `WebService.get()` might be:
```java
// Transports define how bytes move to and from their destination
SocketTransport transport = new SocketTransport("thrift.google.com", 80);
transport.connect();
// Protocols define the mapping between structs and bytes
Protocol protocol = new BinaryProtocol(transport);
// Generated clients do the plumbing
Google client = new WebServiceClient(protocol);
Query query = new Query.Builder()
.text("thrift vs protocol buffers")
.build();
// RPC clients are asynchronous and callback-based
client.search(query, new ServiceMethodCallback<List<SearchResult>() {
@Override
public void onSuccess(List<SearchResult> response) {
// yay
}
@Override
public void onError(Throwable error) {
Log.e("WebService", "Search error: " + error);
}
});
```
### Building
```bash
@ -72,6 +336,11 @@ The major differences are:
You will need to have valid Sonatype Nexus OSS credentials, as well as a valid *and published* GPG signing key, configured in your local `gradle.properties` file.
### Thanks
Thrifty owes an enormous debt to Square and the Wire team; without them, this project would not exist. Thanks!
An equal debt is owed to Facebook and Apache for developing and opening Thrift to the world.
-------
Copyright © 2015 Benjamin Bader

Просмотреть файл

@ -17,11 +17,6 @@ public class BinaryProtocolConformance extends ConformanceBase {
return ServerProtocol.BINARY;
}
@Override
protected Transport decorate(Transport transport) {
return transport;
}
@Override
protected Protocol createProtocol(Transport transport) {
return new BinaryProtocol(transport);

Просмотреть файл

@ -17,11 +17,6 @@ public class CompactProtocolConformance extends ConformanceBase {
return ServerProtocol.COMPACT;
}
@Override
protected Transport decorate(Transport transport) {
return transport;
}
@Override
protected Protocol createProtocol(Transport transport) {
return new CompactProtocol(transport);

Просмотреть файл

@ -60,6 +60,13 @@ import static org.junit.Assert.assertThat;
* generated by their compiler.
*/
public abstract class ConformanceBase {
/**
* An Apache Thrift server that is started anew for each test.
*
* <p>The server's transport and protocols are configured based
* on values returned by the abstract methods
* {@link #getServerProtocol()} and {@link #getServerTransport()}.
*/
@Rule public TestServer testServer;
private Transport transport;
@ -81,7 +88,7 @@ public abstract class ConformanceBase {
transport.connect();
this.transport = decorate(transport);
this.transport = decorateTransport(transport);
this.protocol = createProtocol(this.transport);
this.client = new ThriftTestClient(protocol, new ClientBase.Listener() {
@Override
@ -107,7 +114,13 @@ public abstract class ConformanceBase {
*/
protected abstract ServerProtocol getServerProtocol();
protected abstract Transport decorate(Transport transport);
/**
* When overridden in a derived class, wraps the given transport
* in a decorator, e.g. a framed transport.
*/
protected Transport decorateTransport(Transport transport) {
return transport;
}
protected abstract Protocol createProtocol(Transport transport);

Просмотреть файл

@ -19,7 +19,7 @@ public class NonblockingBinaryProtocolConformance extends ConformanceBase {
}
@Override
protected Transport decorate(Transport transport) {
protected Transport decorateTransport(Transport transport) {
// non-blocking servers require framing
return new FramedTransport(transport);
}

Просмотреть файл

@ -19,7 +19,7 @@ public class NonblockingCompactProtocolConformance extends ConformanceBase {
}
@Override
protected Transport decorate(Transport transport) {
protected Transport decorateTransport(Transport transport) {
return new FramedTransport(transport);
}

Просмотреть файл

@ -2,5 +2,5 @@ thrifty-test-server
-------------------
A configurable server based on the TestThrift.thrift test data, used for conformance tests
in `thrifty-runtime`.
in `thrifty-integration-tests`.