Repository: hamba/avro
Pull request #3 (2019-03-06 15:35:57)

Registry


Patch:
diff --git a/README.md b/README.md
index 2d652be..8e10695 100644
--- a/README.md
+++ b/README.md
@@ -113,7 +113,6 @@ Always benchmark with your own workload. The result depends heavily on the data
 
 * Improve test coverage, docs and examples
 * Logical Types
-* Schema registry
 * Schema
     * Refactor parsing to be cleaner
     * Better schema validation
diff --git a/container/container.go b/container/container.go
index b6edbc6..edcc6af 100644
--- a/container/container.go
+++ b/container/container.go
@@ -3,7 +3,7 @@ Package container implements encoding and decoding of Avro Object Container File
 
 See the Avro specification for an understanding of Avro: http://avro.apache.org/docs/current/
 
- */
+*/
 package container
 
 import (
@@ -85,6 +85,7 @@ func NewDecoder(r io.Reader) (*Decoder, error) {
 	}, nil
 }
 
+// HasNext determines if there is another value to read.
 func (d *Decoder) HasNext() bool {
 	if d.count <= 0 {
 		count, _ := d.readBlock() // err handled in Error function
diff --git a/go.sum b/go.sum
index baf4c38..eed0add 100644
--- a/go.sum
+++ b/go.sum
@@ -8,6 +8,7 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
diff --git a/registry/client.go b/registry/client.go
new file mode 100644
index 0000000..40a0be5
--- /dev/null
+++ b/registry/client.go
@@ -0,0 +1,242 @@
+/*
+Package registry implements a Confluent Schema Registry compliant client.
+
+See the Confluent Schema Registry docs for an understanding of the API: https://docs.confluent.io/current/schema-registry/docs/api.html
+
+*/
+package registry
+
+import (
+	"bytes"
+	"io"
+	"net"
+	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/hamba/avro"
+	"github.com/json-iterator/go"
+	"github.com/modern-go/concurrent"
+)
+
+const (
+	contentType = "application/vnd.schemaregistry.v1+json"
+)
+
+// Registry represents a schema registry.
+type Registry interface {
+	// GetSchema returns the schema with the given id.
+	GetSchema(id int) (avro.Schema, error)
+
+	// GetSubjects gets the registry subjects.
+	GetSubjects() ([]string, error)
+
+	// GetVersions gets the schema versions for a subject.
+	GetVersions(subject string) ([]int, error)
+
+	// GetSchemaByVersion gets the schema by version.
+	GetSchemaByVersion(subject string, version int) (avro.Schema, error)
+
+	// GetLatestSchema gets the latest schema for a subject.
+	GetLatestSchema(subject string) (avro.Schema, error)
+
+	// CreateSchema creates a schema in the registry, returning the schema id.
+	CreateSchema(subject string, schema string) (int, avro.Schema, error)
+
+	// IsRegistered determines of the schema is registered.
+	IsRegistered(subject string, schema string) (int, avro.Schema, error)
+}
+
+type schemaPayload struct {
+	Schema string `json:"schema"`
+}
+
+type idPayload struct {
+	ID int `json:"id"`
+}
+
+var defaultClient = &http.Client{
+	Transport: &http.Transport{
+		Proxy: http.ProxyFromEnvironment,
+		DialContext: (&net.Dialer{
+			Timeout:   15 * time.Second,
+			KeepAlive: 90 * time.Second,
+		}).DialContext,
+		TLSHandshakeTimeout: 3 * time.Second,
+	},
+}
+
+// ClientFunc is a function used to customize the Client.
+type ClientFunc func(*Client)
+
+// WithHTTPClient sets the http client to make requests with.
+func WithHTTPClient(client *http.Client) ClientFunc {
+	return func(c *Client) {
+		c.client = client
+	}
+}
+
+// Client is an HTTP registry client
+type Client struct {
+	client *http.Client
+	base   string
+
+	cache *concurrent.Map // map[int]avro.Schema
+}
+
+// NewClient creates a schema registry Client with the given base url.
+func NewClient(baseURL string, opts ...ClientFunc) (*Client, error) {
+	_, err := url.Parse(baseURL)
+	if err != nil {
+		return nil, err
+	}
+	baseURL = strings.TrimSuffix(baseURL, "/")
+
+	c := &Client{
+		client: defaultClient,
+		base:   baseURL,
+		cache:  concurrent.NewMap(),
+	}
+
+	for _, opt := range opts {
+		opt(c)
+	}
+
+	return c, nil
+}
+
+// GetSchema returns the schema with the given id.
+//
+// GetSchema will cache the schema in memory after it is successfully returned,
+// allowing it to be used efficiently in a high load situation.
+func (c *Client) GetSchema(id int) (avro.Schema, error) {
+	if schema, ok := c.cache.Load(id); ok {
+		return schema.(avro.Schema), nil
+	}
+
+	var payload schemaPayload
+	err := c.request(http.MethodGet, "/schemas/ids/"+strconv.Itoa(id), nil, &payload)
+	if err != nil {
+		return nil, err
+	}
+
+	schema, err := avro.Parse(payload.Schema)
+	if err != nil {
+		return nil, err
+	}
+
+	c.cache.Store(id, schema)
+
+	return schema, nil
+}
+
+// GetSubjects gets the registry subjects.
+func (c *Client) GetSubjects() ([]string, error) {
+	var subjects []string
+	err := c.request(http.MethodGet, "/subjects", nil, &subjects)
+	if err != nil {
+		return nil, err
+	}
+
+	return subjects, err
+}
+
+// GetVersions gets the schema versions for a subject.
+func (c *Client) GetVersions(subject string) ([]int, error) {
+	var versions []int
+	err := c.request(http.MethodGet, "/subjects/"+subject+"/versions", nil, &versions)
+	if err != nil {
+		return nil, err
+	}
+
+	return versions, err
+}
+
+// GetSchemaByVersion gets the schema by version.
+func (c *Client) GetSchemaByVersion(subject string, version int) (avro.Schema, error) {
+	var payload schemaPayload
+	err := c.request(http.MethodGet, "/subjects/"+subject+"/versions/"+strconv.Itoa(version), nil, &payload)
+	if err != nil {
+		return nil, err
+	}
+
+	return avro.Parse(payload.Schema)
+}
+
+// GetLatestSchema gets the latest schema for a subject.
+func (c *Client) GetLatestSchema(subject string) (avro.Schema, error) {
+	var payload schemaPayload
+	err := c.request(http.MethodGet, "/subjects/"+subject+"/versions/latest", nil, &payload)
+	if err != nil {
+		return nil, err
+	}
+
+	return avro.Parse(payload.Schema)
+}
+
+// CreateSchema creates a schema in the registry, returning the schema id.
+func (c *Client) CreateSchema(subject string, schema string) (int, avro.Schema, error) {
+	var payload idPayload
+	err := c.request(http.MethodPost, "/subjects/"+subject+"/versions", schemaPayload{Schema: schema}, &payload)
+	if err != nil {
+		return 0, nil, err
+	}
+
+	sch, err := avro.Parse(schema)
+	return payload.ID, sch, err
+}
+
+// IsRegistered determines of the schema is registered.
+func (c *Client) IsRegistered(subject string, schema string) (int, avro.Schema, error) {
+	var payload idPayload
+	err := c.request(http.MethodPost, "/subjects/"+subject, schemaPayload{Schema: schema}, &payload)
+	if err != nil {
+		return 0, nil, err
+	}
+
+	sch, err := avro.Parse(schema)
+	return payload.ID, sch, err
+}
+
+func (c *Client) request(method, uri string, in, out interface{}) error {
+	var body io.Reader
+	if in != nil {
+		b, _ := jsoniter.Marshal(in)
+		body = bytes.NewReader(b)
+	}
+
+	req, _ := http.NewRequest(method, c.base+uri, body) // This error is not possible as we already parsed the url
+	req.Header.Set("Content-Type", contentType)
+
+	resp, err := c.client.Do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode >= 400 {
+		err := Error{StatusCode: resp.StatusCode}
+		jsoniter.NewDecoder(resp.Body).Decode(err)
+		return err
+	}
+
+	return jsoniter.NewDecoder(resp.Body).Decode(out)
+}
+
+// Error is returned by the registry when there is an error.
+type Error struct {
+	StatusCode int `json:"-"`
+
+	Code    int    `json:"error_code"`
+	Message string `json:"message"`
+}
+
+// Error returns the error message.
+func (e Error) Error() string {
+	return e.Message
+}
+
+
+


Test patch:
diff --git a/registry/client_internal_test.go b/registry/client_internal_test.go
new file mode 100644
index 0000000..cdc4c9e
--- /dev/null
+++ b/registry/client_internal_test.go
@@ -0,0 +1,16 @@
+package registry
+
+import (
+	"net/http"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewClient_WithHTTPClient(t *testing.T) {
+	httpClient := &http.Client{}
+
+	client, _ := NewClient("http://example.com", WithHTTPClient(httpClient))
+
+	assert.Equal(t, client.client, httpClient)
+}
diff --git a/registry/client_test.go b/registry/client_test.go
new file mode 100644
index 0000000..f0ae724
--- /dev/null
+++ b/registry/client_test.go
@@ -0,0 +1,405 @@
+package registry_test
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/hamba/avro/registry"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewClient(t *testing.T) {
+	client, err := registry.NewClient("http://example.com")
+
+	assert.NoError(t, err)
+	assert.Implements(t, (*registry.Registry)(nil), client)
+}
+
+func TestNewClient_UrlError(t *testing.T) {
+	_, err := registry.NewClient("://")
+
+	assert.Error(t, err)
+}
+
+func TestClient_GetSchema(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, "GET", r.Method)
+		assert.Equal(t, "/schemas/ids/5", r.URL.Path)
+
+		_, _ = w.Write([]byte(`{"schema":"[\"null\",\"string\",\"int\"]"}`))
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	schema, err := client.GetSchema(5)
+
+	assert.NoError(t, err)
+	assert.Equal(t, `["null","string","int"]`, schema.String())
+}
+
+func TestClient_GetSchemaCachesSchema(t *testing.T) {
+	count := 0
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		count++
+		_, _ = w.Write([]byte(`{"schema":"[\"null\",\"string\",\"int\"]"}`))
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	_, _ = client.GetSchema(5)
+
+	_, _ = client.GetSchema(5)
+
+	assert.Equal(t, 1, count)
+}
+
+func TestClient_GetSchemaRequestError(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(500)
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	_, err := client.GetSchema(5)
+
+	assert.Error(t, err)
+}
+
+func TestClient_GetSchemaJsonError(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		_, _ = w.Write([]byte(`{"schema":"[\"null\",\"string\",\"int\"]"`))
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	_, err := client.GetSchema(5)
+
+	assert.Error(t, err)
+}
+
+func TestClient_GetSchemaSchemaError(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		_, _ = w.Write([]byte(`{"schema":""}`))
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	_, err := client.GetSchema(5)
+
+	assert.Error(t, err)
+}
+
+func TestClient_GetSubjects(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, "GET", r.Method)
+		assert.Equal(t, "/subjects", r.URL.Path)
+
+		_, _ = w.Write([]byte(`["foobar"]`))
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	subs, err := client.GetSubjects()
+
+	assert.NoError(t, err)
+	assert.Len(t, subs, 1)
+	assert.Equal(t, "foobar", subs[0])
+}
+
+func TestClient_GetSubjectsRequestError(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(500)
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	_, err := client.GetSubjects()
+
+	assert.Error(t, err)
+}
+
+func TestClient_GetSubjectsJSONError(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		_, _ = w.Write([]byte(`["foobar"`))
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	_, err := client.GetSubjects()
+
+	assert.Error(t, err)
+}
+
+func TestClient_GetVersions(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, "GET", r.Method)
+		assert.Equal(t, "/subjects/foobar/versions", r.URL.Path)
+
+		_, _ = w.Write([]byte(`[10]`))
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	vers, err := client.GetVersions("foobar")
+
+	assert.NoError(t, err)
+	assert.Len(t, vers, 1)
+	assert.Equal(t, 10, vers[0])
+}
+
+func TestClient_GetVersionsRequestError(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(500)
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	_, err := client.GetVersions("foobar")
+
+	assert.Error(t, err)
+}
+
+func TestClient_GetVersionsJSONError(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		_, _ = w.Write([]byte(`[10`))
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	_, err := client.GetVersions("foobar")
+
+	assert.Error(t, err)
+}
+
+func TestClient_GetSchemaByVersion(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, "GET", r.Method)
+		assert.Equal(t, "/subjects/foobar/versions/5", r.URL.Path)
+
+		_, _ = w.Write([]byte(`{"schema":"[\"null\",\"string\",\"int\"]"}`))
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	schema, err := client.GetSchemaByVersion("foobar", 5)
+
+	assert.NoError(t, err)
+	assert.Equal(t, `["null","string","int"]`, schema.String())
+}
+
+func TestClient_GetSchemaByVersionRequestError(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(500)
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	_, err := client.GetSchemaByVersion("foobar", 5)
+
+	assert.Error(t, err)
+}
+
+func TestClient_GetSchemaByVersionJsonError(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		_, _ = w.Write([]byte(`{"schema":"[\"null\",\"string\",\"int\"]"`))
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	_, err := client.GetSchemaByVersion("foobar", 5)
+
+	assert.Error(t, err)
+}
+
+func TestClient_GetSchemaByVersionSchemaError(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		_, _ = w.Write([]byte(`{"schema":""}`))
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	_, err := client.GetSchemaByVersion("foobar", 5)
+
+	assert.Error(t, err)
+}
+
+func TestClient_GetLatestSchema(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, "GET", r.Method)
+		assert.Equal(t, "/subjects/foobar/versions/latest", r.URL.Path)
+
+		_, _ = w.Write([]byte(`{"schema":"[\"null\",\"string\",\"int\"]"}`))
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	schema, err := client.GetLatestSchema("foobar")
+
+	assert.NoError(t, err)
+	assert.Equal(t, `["null","string","int"]`, schema.String())
+}
+
+func TestClient_GetLatestSchemaRequestError(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(500)
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	_, err := client.GetLatestSchema("foobar")
+
+	assert.Error(t, err)
+}
+
+func TestClient_GetLatestSchemaJsonError(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		_, _ = w.Write([]byte(`{"schema":"[\"null\",\"string\",\"int\"]"`))
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	_, err := client.GetLatestSchema("foobar")
+
+	assert.Error(t, err)
+}
+
+func TestClient_GetLatestSchemaSchemaError(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		_, _ = w.Write([]byte(`{"schema":""}`))
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	_, err := client.GetLatestSchema("foobar")
+
+	assert.Error(t, err)
+}
+
+func TestClient_CreateSchema(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, "POST", r.Method)
+		assert.Equal(t, "/subjects/foobar/versions", r.URL.Path)
+
+		_, _ = w.Write([]byte(`{"id":10}`))
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	id, schema, err := client.CreateSchema("foobar", "[\"null\",\"string\",\"int\"]")
+
+	assert.NoError(t, err)
+	assert.Equal(t, 10, id)
+	assert.Equal(t, `["null","string","int"]`, schema.String())
+}
+
+func TestClient_CreateSchemaRequestError(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(500)
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	_, _, err := client.CreateSchema("foobar", "[\"null\",\"string\",\"int\"]")
+
+	assert.Error(t, err)
+}
+
+func TestClient_CreateSchemaJsonError(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		_, _ = w.Write([]byte(`{"id":10`))
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	_, _, err := client.CreateSchema("foobar", "[\"null\",\"string\",\"int\"]")
+
+	assert.Error(t, err)
+}
+
+func TestClient_CreateSchemaSchemaError(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		_, _ = w.Write([]byte(`{"id":10}`))
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	_, _, err := client.CreateSchema("foobar", "[\"null\",\"string\",\"int\"")
+
+	assert.Error(t, err)
+}
+
+func TestClient_IsRegistered(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, "POST", r.Method)
+		assert.Equal(t, "/subjects/test", r.URL.Path)
+
+		_, _ = w.Write([]byte(`{"id":10}`))
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	id, schema, err := client.IsRegistered("test", "[\"null\",\"string\",\"int\"]")
+
+	assert.NoError(t, err)
+	assert.Equal(t, 10, id)
+	assert.Equal(t, `["null","string","int"]`, schema.String())
+}
+
+func TestClient_IsRegisteredRequestError(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(500)
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	_, _, err := client.IsRegistered("test", "[\"null\",\"string\",\"int\"]")
+
+	assert.Error(t, err)
+}
+
+func TestClient_IsRegisteredJsonError(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		_, _ = w.Write([]byte(`{"id":10`))
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	_, _, err := client.IsRegistered("test", "[\"null\",\"string\",\"int\"]")
+
+	assert.Error(t, err)
+}
+
+func TestClient_IsRegisteredSchemaError(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		_, _ = w.Write([]byte(`{"id":10}`))
+	}))
+	defer s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	_, _, err := client.IsRegistered("test", "[\"null\",\"string\",\"int\"")
+
+	assert.Error(t, err)
+}
+
+func TestClient_HandlesServerError(t *testing.T) {
+	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
+	s.Close()
+	client, _ := registry.NewClient(s.URL)
+
+	_, err := client.GetSchema(5)
+
+	assert.Error(t, err)
+}
+
+func TestError_Error(t *testing.T) {
+	err := registry.Error{
+		StatusCode: 404,
+		Code: 40403,
+		Message: "Schema not found",
+	}
+
+	str := err.Error()
+
+	assert.Equal(t, "Schema not found", str)
+}
diff --git a/registry/example_test.go b/registry/example_test.go
new file mode 100644
index 0000000..88b561a
--- /dev/null
+++ b/registry/example_test.go
@@ -0,0 +1,31 @@
+package registry_test
+
+import (
+	"fmt"
+	"log"
+
+	"github.com/hamba/avro/registry"
+)
+
+func Example() {
+	reg, err := registry.NewClient("http://example.com")
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	schema, err := reg.GetSchema(5)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	fmt.Println("schema: ", schema)
+
+	schemaRaw := `["null","string","int"]`
+	id, schema, err := reg.IsRegistered("foobar", schemaRaw)
+	if err != nil {
+		id, schema, err = reg.CreateSchema("foobar", schemaRaw)
+	}
+
+	fmt.Println("id: ", id)
+	fmt.Println("schema: ", schema)
+}