github.com/landoop/schema-registry@v0.0.0-20190327143759-50a5701c1891/client.go (about)

     1  // Package schemaregistry provides a client for Confluent's Kafka Schema Registry REST API.
     2  package schemaregistry
     3  
     4  import (
     5  	"bytes"
     6  	"compress/gzip"
     7  	"crypto/tls"
     8  	"encoding/json"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"net"
    13  	"net/http"
    14  	"net/url"
    15  	"strings"
    16  	"time"
    17  )
    18  
    19  // DefaultURL is the address where a local schema registry listens by default.
    20  const DefaultURL = "http://localhost:8081"
    21  
    22  type (
    23  	httpDoer interface {
    24  		Do(req *http.Request) (resp *http.Response, err error)
    25  	}
    26  	// Client is the registry schema REST API client.
    27  	Client struct {
    28  		baseURL string
    29  
    30  		// the client is created on the `NewClient` function, it can be customized via options.
    31  		client httpDoer
    32  	}
    33  
    34  	// Option describes an optional runtime configurator that can be passed on `NewClient`.
    35  	// Custom `Option` can be used as well, it's just a type of `func(*schemaregistry.Client)`.
    36  	//
    37  	// Look `UsingClient`.
    38  	Option func(*Client)
    39  )
    40  
    41  // UsingClient modifies the underline HTTP Client that schema registry is using for contact with the backend server.
    42  func UsingClient(httpClient *http.Client) Option {
    43  	return func(c *Client) {
    44  		if httpClient == nil {
    45  			return
    46  		}
    47  
    48  		transport := getTransportLayer(httpClient, 0)
    49  		httpClient.Transport = transport
    50  
    51  		c.client = httpClient
    52  	}
    53  }
    54  
    55  func getTransportLayer(httpClient *http.Client, timeout time.Duration) (t http.RoundTripper) {
    56  	if t := httpClient.Transport; t != nil {
    57  		return t
    58  	}
    59  
    60  	httpTransport := &http.Transport{
    61  		TLSNextProto: make(map[string]func(authority string, c *tls.Conn) http.RoundTripper),
    62  	}
    63  
    64  	if timeout > 0 {
    65  		httpTransport.Dial = func(network string, addr string) (net.Conn, error) {
    66  			return net.DialTimeout(network, addr, timeout)
    67  		}
    68  	}
    69  
    70  	return httpTransport
    71  }
    72  
    73  // formatBaseURL will try to make sure that the schema:host:port pattern is followed on the `baseURL` field.
    74  func formatBaseURL(baseURL string) string {
    75  	if baseURL == "" {
    76  		return ""
    77  	}
    78  
    79  	// remove last slash, so the API can append the path with ease.
    80  	if baseURL[len(baseURL)-1] == '/' {
    81  		baseURL = baseURL[0 : len(baseURL)-1]
    82  	}
    83  
    84  	portIdx := strings.LastIndexByte(baseURL, ':')
    85  
    86  	schemaIdx := strings.Index(baseURL, "://")
    87  	hasSchema := schemaIdx >= 0
    88  	hasPort := portIdx > schemaIdx+1
    89  
    90  	var port = "80"
    91  	if hasPort {
    92  		port = baseURL[portIdx+1:]
    93  	}
    94  
    95  	// find the schema based on the port.
    96  	if !hasSchema {
    97  		if port == "443" {
    98  			baseURL = "https://" + baseURL
    99  		} else {
   100  			baseURL = "http://" + baseURL
   101  		}
   102  	} else if !hasPort {
   103  		// has schema but not port.
   104  		if strings.HasPrefix(baseURL, "https://") {
   105  			port = "443"
   106  		}
   107  	}
   108  
   109  	// finally, append the port part if it wasn't there.
   110  	if !hasPort {
   111  		baseURL += ":" + port
   112  	}
   113  
   114  	return baseURL
   115  }
   116  
   117  // NewClient creates & returns a new Registry Schema Client
   118  // based on the passed url and the options.
   119  func NewClient(baseURL string, options ...Option) (*Client, error) {
   120  	baseURL = formatBaseURL(baseURL)
   121  	if _, err := url.Parse(baseURL); err != nil {
   122  		return nil, err
   123  	}
   124  
   125  	c := &Client{baseURL: baseURL}
   126  	for _, opt := range options {
   127  		opt(c)
   128  	}
   129  
   130  	if c.client == nil {
   131  		httpClient := &http.Client{}
   132  		UsingClient(httpClient)(c)
   133  	}
   134  
   135  	return c, nil
   136  }
   137  
   138  const (
   139  	contentTypeHeaderKey = "Content-Type"
   140  	contentTypeJSON      = "application/json"
   141  
   142  	acceptHeaderKey          = "Accept"
   143  	acceptEncodingHeaderKey  = "Accept-Encoding"
   144  	contentEncodingHeaderKey = "Content-Encoding"
   145  	gzipEncodingHeaderValue  = "gzip"
   146  )
   147  
   148  // ResourceError is being fired from all API calls when an error code is received.
   149  type ResourceError struct {
   150  	ErrorCode int    `json:"error_code"`
   151  	Method    string `json:"method,omitempty"`
   152  	URI       string `json:"uri,omitempty"`
   153  	Message   string `json:"message,omitempty"`
   154  }
   155  
   156  func (err ResourceError) Error() string {
   157  	return fmt.Sprintf("client: (%s: %s) failed with error code %d%s",
   158  		err.Method, err.URI, err.ErrorCode, err.Message)
   159  }
   160  
   161  func newResourceError(errCode int, uri, method, body string) ResourceError {
   162  	unescapedURI, _ := url.QueryUnescape(uri)
   163  
   164  	return ResourceError{
   165  		ErrorCode: errCode,
   166  		URI:       unescapedURI,
   167  		Method:    method,
   168  		Message:   body,
   169  	}
   170  }
   171  
   172  // These numbers are used by the schema registry to communicate errors.
   173  const (
   174  	subjectNotFoundCode = 40401
   175  	schemaNotFoundCode  = 40403
   176  )
   177  
   178  // IsSubjectNotFound checks the returned error to see if it is kind of a subject not found  error code.
   179  func IsSubjectNotFound(err error) bool {
   180  	if err == nil {
   181  		return false
   182  	}
   183  
   184  	if resErr, ok := err.(ResourceError); ok {
   185  		return resErr.ErrorCode == subjectNotFoundCode
   186  	}
   187  
   188  	return false
   189  }
   190  
   191  // IsSchemaNotFound checks the returned error to see if it is kind of a schema not found error code.
   192  func IsSchemaNotFound(err error) bool {
   193  	if err == nil {
   194  		return false
   195  	}
   196  
   197  	if resErr, ok := err.(ResourceError); ok {
   198  		return resErr.ErrorCode == schemaNotFoundCode
   199  	}
   200  
   201  	return false
   202  }
   203  
   204  // isOK is called inside the `Client#do` and it closes the body reader if no accessible.
   205  func isOK(resp *http.Response) bool {
   206  	return !(resp.StatusCode < 200 || resp.StatusCode >= 300)
   207  }
   208  
   209  var noOpBuffer = new(bytes.Buffer)
   210  
   211  func acquireBuffer(b []byte) *bytes.Buffer {
   212  	if len(b) > 0 {
   213  		return bytes.NewBuffer(b)
   214  	}
   215  
   216  	return noOpBuffer
   217  }
   218  
   219  const schemaAPIVersion = "v1"
   220  const contentTypeSchemaJSON = "application/vnd.schemaregistry." + schemaAPIVersion + "+json"
   221  
   222  func (c *Client) do(method, path, contentType string, send []byte) (*http.Response, error) {
   223  	if path[0] == '/' {
   224  		path = path[1:]
   225  	}
   226  
   227  	uri := c.baseURL + "/" + path
   228  
   229  	req, err := http.NewRequest(method, uri, acquireBuffer(send))
   230  	if err != nil {
   231  		return nil, err
   232  	}
   233  
   234  	// set the content type if any.
   235  	if contentType != "" {
   236  		req.Header.Set(contentTypeHeaderKey, contentType)
   237  	}
   238  
   239  	// response accept gziped content.
   240  	req.Header.Add(acceptEncodingHeaderKey, gzipEncodingHeaderValue)
   241  	req.Header.Add(acceptHeaderKey, contentTypeSchemaJSON+", application/vnd.schemaregistry+json, application/json")
   242  
   243  	// send the request and check the response for any connection & authorization errors here.
   244  	resp, err := c.client.Do(req)
   245  	if err != nil {
   246  		return nil, err
   247  	}
   248  
   249  	if !isOK(resp) {
   250  		defer resp.Body.Close()
   251  		var errBody string
   252  		respContentType := resp.Header.Get(contentTypeHeaderKey)
   253  
   254  		if strings.Contains(respContentType, "text/html") {
   255  			// if the body is html, then don't read it, it doesn't contain the raw info we need.
   256  		} else if strings.Contains(respContentType, "json") {
   257  			// if it's json try to read it as confluent's specific error json.
   258  			var resErr ResourceError
   259  			c.readJSON(resp, &resErr)
   260  			return nil, resErr
   261  		} else {
   262  			// else give the whole body to the error context.
   263  			b, err := c.readResponseBody(resp)
   264  			if err != nil {
   265  				errBody = " unable to read body: " + err.Error()
   266  			} else {
   267  				errBody = "\n" + string(b)
   268  			}
   269  		}
   270  
   271  		return nil, newResourceError(resp.StatusCode, uri, method, errBody)
   272  	}
   273  
   274  	return resp, nil
   275  }
   276  
   277  type gzipReadCloser struct {
   278  	respReader io.ReadCloser
   279  	gzipReader io.ReadCloser
   280  }
   281  
   282  func (rc *gzipReadCloser) Close() error {
   283  	if rc.gzipReader != nil {
   284  		defer rc.gzipReader.Close()
   285  	}
   286  
   287  	return rc.respReader.Close()
   288  }
   289  
   290  func (rc *gzipReadCloser) Read(p []byte) (n int, err error) {
   291  	if rc.gzipReader != nil {
   292  		return rc.gzipReader.Read(p)
   293  	}
   294  
   295  	return rc.respReader.Read(p)
   296  }
   297  
   298  func (c *Client) acquireResponseBodyStream(resp *http.Response) (io.ReadCloser, error) {
   299  	// check for gzip and read it, the right way.
   300  	var (
   301  		reader = resp.Body
   302  		err    error
   303  	)
   304  
   305  	if encoding := resp.Header.Get(contentEncodingHeaderKey); encoding == gzipEncodingHeaderValue {
   306  		reader, err = gzip.NewReader(resp.Body)
   307  		if err != nil {
   308  			return nil, fmt.Errorf("client: failed to read gzip compressed content, trace: %v", err)
   309  		}
   310  		// we wrap the gzipReader and the underline response reader
   311  		// so a call of .Close() can close both of them with the correct order when finish reading, the caller decides.
   312  		// Must close manually using a defer on the callers before the `readResponseBody` call,
   313  		// note that the `readJSON` can decide correctly by itself.
   314  		return &gzipReadCloser{
   315  			respReader: resp.Body,
   316  			gzipReader: reader,
   317  		}, nil
   318  	}
   319  
   320  	// return the stream reader.
   321  	return reader, err
   322  }
   323  
   324  func (c *Client) readResponseBody(resp *http.Response) ([]byte, error) {
   325  	reader, err := c.acquireResponseBodyStream(resp)
   326  	if err != nil {
   327  		return nil, err
   328  	}
   329  
   330  	body, err := ioutil.ReadAll(reader)
   331  	if err = reader.Close(); err != nil {
   332  		return nil, err
   333  	}
   334  
   335  	// return the body.
   336  	return body, err
   337  }
   338  
   339  func (c *Client) readJSON(resp *http.Response, valuePtr interface{}) error {
   340  	b, err := c.readResponseBody(resp)
   341  	if err != nil {
   342  		return err
   343  	}
   344  
   345  	return json.Unmarshal(b, valuePtr)
   346  }
   347  
   348  var errRequired = func(field string) error {
   349  	return fmt.Errorf("client: %s is required", field)
   350  }
   351  
   352  const (
   353  	subjectsPath = "subjects"
   354  	subjectPath  = subjectsPath + "/%s"
   355  	schemaPath   = "schemas/ids/%d"
   356  )
   357  
   358  // Subjects returns a list of the available subjects(schemas).
   359  // https://docs.confluent.io/current/schema-registry/docs/api.html#subjects
   360  func (c *Client) Subjects() (subjects []string, err error) {
   361  	// # List all available subjects
   362  	// GET /subjects
   363  	resp, respErr := c.do(http.MethodGet, subjectsPath, "", nil)
   364  	if respErr != nil {
   365  		err = respErr
   366  		return
   367  	}
   368  
   369  	err = c.readJSON(resp, &subjects)
   370  	return
   371  }
   372  
   373  // Versions returns all schema version numbers registered for this subject.
   374  func (c *Client) Versions(subject string) (versions []int, err error) {
   375  	if subject == "" {
   376  		err = errRequired("subject")
   377  		return
   378  	}
   379  
   380  	// # List all versions of a particular subject
   381  	// GET /subjects/(string: subject)/versions
   382  	path := fmt.Sprintf(subjectPath, subject+"/versions")
   383  	resp, respErr := c.do(http.MethodGet, path, "", nil)
   384  	if respErr != nil {
   385  		err = respErr
   386  		return
   387  	}
   388  
   389  	err = c.readJSON(resp, &versions)
   390  	return
   391  }
   392  
   393  // DeleteSubject deletes the specified subject and its associated compatibility level if registered.
   394  // It is recommended to use this API only when a topic needs to be recycled or in development environment.
   395  // Returns the versions of the schema deleted under this subject.
   396  func (c *Client) DeleteSubject(subject string) (versions []int, err error) {
   397  	if subject == "" {
   398  		err = errRequired("subject")
   399  		return
   400  	}
   401  
   402  	// DELETE /subjects/(string: subject)
   403  	path := fmt.Sprintf(subjectPath, subject)
   404  	resp, respErr := c.do(http.MethodDelete, path, "", nil)
   405  	if respErr != nil {
   406  		err = respErr
   407  		return
   408  	}
   409  
   410  	err = c.readJSON(resp, &versions)
   411  	return
   412  }
   413  
   414  // IsRegistered tells if the given "schema" is registered for this "subject".
   415  func (c *Client) IsRegistered(subject, schema string) (bool, Schema, error) {
   416  	var fs Schema
   417  
   418  	sc := schemaOnlyJSON{schema}
   419  	send, err := json.Marshal(sc)
   420  	if err != nil {
   421  		return false, fs, err
   422  	}
   423  
   424  	path := fmt.Sprintf(subjectPath, subject)
   425  	resp, err := c.do(http.MethodPost, path, "", send)
   426  	if err != nil {
   427  		// schema not found?
   428  		if IsSchemaNotFound(err) {
   429  			return false, fs, nil
   430  		}
   431  		// error?
   432  		return false, fs, err
   433  	}
   434  
   435  	if err = c.readJSON(resp, &fs); err != nil {
   436  		return true, fs, err // found but error when unmarshal.
   437  	}
   438  
   439  	// so we have a schema.
   440  	return true, fs, nil
   441  }
   442  
   443  type (
   444  	schemaOnlyJSON struct {
   445  		Schema string `json:"schema"`
   446  	}
   447  
   448  	idOnlyJSON struct {
   449  		ID int `json:"id"`
   450  	}
   451  
   452  	isCompatibleJSON struct {
   453  		IsCompatible bool `json:"is_compatible"`
   454  	}
   455  
   456  	// Schema describes a schema, look `GetSchema` for more.
   457  	Schema struct {
   458  		// Schema is the Avro schema string.
   459  		Schema string `json:"schema"`
   460  		// Subject where the schema is registered for.
   461  		Subject string `json:"subject"`
   462  		// Version of the returned schema.
   463  		Version int `json:"version"`
   464  		ID      int `json:"id,omitempty"`
   465  	}
   466  
   467  	// Config describes a subject or globa schema-registry configuration
   468  	Config struct {
   469  		// CompatibilityLevel mode of subject or global
   470  		CompatibilityLevel string `json:"compatibilityLevel"`
   471  	}
   472  )
   473  
   474  // RegisterNewSchema registers a schema.
   475  // The returned identifier should be used to retrieve
   476  // this schema from the schemas resource and is different from
   477  // the schema’s version which is associated with that name.
   478  func (c *Client) RegisterNewSchema(subject string, avroSchema string) (int, error) {
   479  	if subject == "" {
   480  		return 0, errRequired("subject")
   481  	}
   482  	if avroSchema == "" {
   483  		return 0, errRequired("avroSchema")
   484  	}
   485  
   486  	schema := schemaOnlyJSON{
   487  		Schema: avroSchema,
   488  	}
   489  
   490  	send, err := json.Marshal(schema)
   491  	if err != nil {
   492  		return 0, err
   493  	}
   494  
   495  	// # Register a new schema under a particular subject
   496  	// POST /subjects/(string: subject)/versions
   497  
   498  	path := fmt.Sprintf(subjectPath+"/versions", subject)
   499  	resp, err := c.do(http.MethodPost, path, contentTypeSchemaJSON, send)
   500  	if err != nil {
   501  		return 0, err
   502  	}
   503  
   504  	var res idOnlyJSON
   505  	err = c.readJSON(resp, &res)
   506  	return res.ID, err
   507  }
   508  
   509  // JSONAvroSchema converts and returns the json form of the "avroSchema" as []byte.
   510  func JSONAvroSchema(avroSchema string) (json.RawMessage, error) {
   511  	var raw json.RawMessage
   512  	err := json.Unmarshal(json.RawMessage(avroSchema), &raw)
   513  	if err != nil {
   514  		return nil, err
   515  	}
   516  	return raw, err
   517  }
   518  
   519  // GetSchemaByID returns the Auro schema string identified by the id.
   520  // id (int) – the globally unique identifier of the schema.
   521  func (c *Client) GetSchemaByID(subjectID int) (string, error) {
   522  	// # Get the schema for a particular subject id
   523  	// GET /schemas/ids/{int: id}
   524  	path := fmt.Sprintf(schemaPath, subjectID)
   525  	resp, err := c.do(http.MethodGet, path, "", nil)
   526  	if err != nil {
   527  		return "", err
   528  	}
   529  
   530  	var res schemaOnlyJSON
   531  	if err = c.readJSON(resp, &res); err != nil {
   532  		return "", err
   533  	}
   534  
   535  	return res.Schema, nil
   536  }
   537  
   538  // SchemaLatestVersion is the only one valid string for the "versionID", it's the "latest" version string and it's used on `GetLatestSchema`.
   539  const SchemaLatestVersion = "latest"
   540  
   541  func checkSchemaVersionID(versionID interface{}) error {
   542  	if versionID == nil {
   543  		return errRequired("versionID (string \"latest\" or int)")
   544  	}
   545  
   546  	if verStr, ok := versionID.(string); ok {
   547  		if verStr != SchemaLatestVersion {
   548  			return fmt.Errorf("client: %v string is not a valid value for the versionID input parameter [versionID == \"latest\"]", versionID)
   549  		}
   550  	}
   551  
   552  	if verInt, ok := versionID.(int); ok {
   553  		if verInt <= 0 || verInt > 2^31-1 { // it's the max of int32, math.MaxInt32 already but do that check.
   554  			return fmt.Errorf("client: %v integer is not a valid value for the versionID input parameter [ versionID > 0 && versionID <= 2^31-1]", versionID)
   555  		}
   556  	}
   557  
   558  	return nil
   559  }
   560  
   561  // subject (string) – Name of the subject
   562  // version (versionId [string "latest" or 1,2^31-1]) – Version of the schema to be returned.
   563  // Valid values for versionId are between [1,2^31-1] or the string “latest”.
   564  // The string “latest” refers to the last registered schema under the specified subject.
   565  // Note that there may be a new latest schema that gets registered right after this request is served.
   566  //
   567  // It's not safe to use just an interface to the high-level API, therefore we split this method
   568  // to two, one which will retrieve the latest versioned schema and the other which will accept
   569  // the version as integer and it will retrieve by a specific version.
   570  //
   571  // See `GetLatestSchema` and `GetSchemaAtVersion` instead.
   572  func (c *Client) getSubjectSchemaAtVersion(subject string, versionID interface{}) (s Schema, err error) {
   573  	if subject == "" {
   574  		err = errRequired("subject")
   575  		return
   576  	}
   577  
   578  	if err = checkSchemaVersionID(versionID); err != nil {
   579  		return
   580  	}
   581  
   582  	// # Get the schema at a particular version
   583  	// GET /subjects/(string: subject)/versions/(versionId: "latest" | int)
   584  	path := fmt.Sprintf(subjectPath+"/versions/%v", subject, versionID)
   585  	resp, respErr := c.do(http.MethodGet, path, "", nil)
   586  	if respErr != nil {
   587  		err = respErr
   588  		return
   589  	}
   590  
   591  	err = c.readJSON(resp, &s)
   592  	return
   593  }
   594  
   595  // GetSchemaBySubject returns the schema for a particular subject and version.
   596  func (c *Client) GetSchemaBySubject(subject string, versionID int) (Schema, error) {
   597  	return c.getSubjectSchemaAtVersion(subject, versionID)
   598  }
   599  
   600  // GetLatestSchema returns the latest version of a schema.
   601  // See `GetSchemaAtVersion` to retrieve a subject schema by a specific version.
   602  func (c *Client) GetLatestSchema(subject string) (Schema, error) {
   603  	return c.getSubjectSchemaAtVersion(subject, SchemaLatestVersion)
   604  }
   605  
   606  // getConfigSubject returns the Config of global or for a given subject. It handles 404 error in a
   607  // different way, since not-found for a subject configuration means it's using global.
   608  func (c *Client) getConfigSubject(subject string) (Config, error) {
   609  	var err error
   610  	var config = Config{}
   611  
   612  	path := fmt.Sprintf("/config/%s", subject)
   613  	resp, respErr := c.do(http.MethodGet, path, "", nil)
   614  	if respErr != nil && respErr.(ResourceError).ErrorCode != 404 {
   615  		return config, respErr
   616  	}
   617  	if resp != nil {
   618  		err = c.readJSON(resp, &config)
   619  	}
   620  
   621  	return config, err
   622  }
   623  
   624  // GetConfig returns the configuration (Config type) for global Schema-Registry or a specific
   625  // subject. When Config returned has "compatibilityLevel" empty, it's using global settings.
   626  func (c *Client) GetConfig(subject string) (Config, error) {
   627  	return c.getConfigSubject(subject)
   628  }
   629  
   630  // subject (string) – Name of the subject
   631  // version (versionId [string "latest" or 1,2^31-1]) – Version of the schema to be returned.
   632  // Valid values for versionId are between [1,2^31-1] or the string “latest”.
   633  // The string “latest” refers to the last registered schema under the specified subject.
   634  // Note that there may be a new latest schema that gets registered right after this request is served.
   635  //
   636  // It's not safe to use just an interface to the high-level API, therefore we split this method
   637  // to two, one which will retrieve the latest versioned schema and the other which will accept
   638  // the version as integer and it will retrieve by a specific version.
   639  //
   640  // See `IsSchemaCompatible` and `IsLatestSchemaCompatible` instead.
   641  func (c *Client) isSchemaCompatibleAtVersion(subject string, avroSchema string, versionID interface{}) (combatible bool, err error) {
   642  	if subject == "" {
   643  		err = errRequired("subject")
   644  		return
   645  	}
   646  	if avroSchema == "" {
   647  		err = errRequired("avroSchema")
   648  		return
   649  	}
   650  
   651  	if err = checkSchemaVersionID(versionID); err != nil {
   652  		return
   653  	}
   654  
   655  	schema := schemaOnlyJSON{
   656  		Schema: avroSchema,
   657  	}
   658  
   659  	send, err := json.Marshal(schema)
   660  	if err != nil {
   661  		return
   662  	}
   663  
   664  	// # Test input schema against a particular version of a subject’s schema for compatibility
   665  	// POST /compatibility/subjects/(string: subject)/versions/(versionId: "latest" | int)
   666  	path := fmt.Sprintf("compatibility/"+subjectPath+"/versions/%v", subject, versionID)
   667  	resp, err := c.do(http.MethodPost, path, contentTypeSchemaJSON, send)
   668  	if err != nil {
   669  		return
   670  	}
   671  
   672  	var res isCompatibleJSON
   673  	err = c.readJSON(resp, &res)
   674  	return res.IsCompatible, err
   675  }
   676  
   677  // IsSchemaCompatible tests compatibility with a specific version of a subject's schema.
   678  func (c *Client) IsSchemaCompatible(subject string, avroSchema string, versionID int) (bool, error) {
   679  	return c.isSchemaCompatibleAtVersion(subject, avroSchema, versionID)
   680  }
   681  
   682  // IsLatestSchemaCompatible tests compatibility with the latest version of a subject's schema.
   683  func (c *Client) IsLatestSchemaCompatible(subject string, avroSchema string) (bool, error) {
   684  	return c.isSchemaCompatibleAtVersion(subject, avroSchema, SchemaLatestVersion)
   685  }