github.com/confluentinc/confluent-kafka-go@v1.9.2/schemaregistry/mock_schemaregistry_client.go (about)

     1  /**
     2   * Copyright 2022 Confluent Inc.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   * http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package schemaregistry
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"net/url"
    23  	"reflect"
    24  	"sort"
    25  	"sync"
    26  )
    27  
    28  const noSubject = ""
    29  
    30  type counter struct {
    31  	count int
    32  }
    33  
    34  func (c counter) currentValue() int {
    35  	return c.count
    36  }
    37  
    38  func (c counter) increment() int {
    39  	c.count++
    40  	return c.count
    41  }
    42  
    43  type versionCacheEntry struct {
    44  	version     int
    45  	softDeleted bool
    46  }
    47  
    48  type idCacheEntry struct {
    49  	id          int
    50  	softDeleted bool
    51  }
    52  
    53  /* HTTP(S) Schema Registry Client and schema caches */
    54  type mockclient struct {
    55  	sync.Mutex
    56  	url                    *url.URL
    57  	schemaCache            map[subjectJSON]idCacheEntry
    58  	schemaCacheLock        sync.RWMutex
    59  	idCache                map[subjectID]*SchemaInfo
    60  	idCacheLock            sync.RWMutex
    61  	versionCache           map[subjectJSON]versionCacheEntry
    62  	versionCacheLock       sync.RWMutex
    63  	compatibilityCache     map[string]Compatibility
    64  	compatibilityCacheLock sync.RWMutex
    65  	counter                counter
    66  }
    67  
    68  var _ Client = new(mockclient)
    69  
    70  // Register registers Schema aliased with subject
    71  func (c *mockclient) Register(subject string, schema SchemaInfo, normalize bool) (id int, err error) {
    72  	schemaJSON, err := schema.MarshalJSON()
    73  	if err != nil {
    74  		return -1, err
    75  	}
    76  	cacheKey := subjectJSON{
    77  		subject: subject,
    78  		json:    string(schemaJSON),
    79  	}
    80  	c.schemaCacheLock.RLock()
    81  	idCacheEntryVal, ok := c.schemaCache[cacheKey]
    82  	if idCacheEntryVal.softDeleted {
    83  		ok = false
    84  	}
    85  	c.schemaCacheLock.RUnlock()
    86  	if ok {
    87  		return id, nil
    88  	}
    89  
    90  	id, err = c.getIDFromRegistry(subject, schema)
    91  	if err != nil {
    92  		return -1, err
    93  	}
    94  	c.schemaCacheLock.Lock()
    95  	c.schemaCache[cacheKey] = idCacheEntry{id, false}
    96  	c.schemaCacheLock.Unlock()
    97  	return id, nil
    98  }
    99  
   100  func (c *mockclient) getIDFromRegistry(subject string, schema SchemaInfo) (int, error) {
   101  	var id = -1
   102  	c.idCacheLock.RLock()
   103  	for key, value := range c.idCache {
   104  		if key.subject == subject && schemasEqual(*value, schema) {
   105  			id = key.id
   106  			break
   107  		}
   108  	}
   109  	c.idCacheLock.RUnlock()
   110  	err := c.generateVersion(subject, schema)
   111  	if err != nil {
   112  		return -1, err
   113  	}
   114  	if id < 0 {
   115  		id = c.counter.increment()
   116  		idCacheKey := subjectID{
   117  			subject: subject,
   118  			id:      id,
   119  		}
   120  		c.idCacheLock.Lock()
   121  		c.idCache[idCacheKey] = &schema
   122  		c.idCacheLock.Unlock()
   123  	}
   124  	return id, nil
   125  }
   126  
   127  func (c *mockclient) generateVersion(subject string, schema SchemaInfo) error {
   128  	versions := c.allVersions(subject)
   129  	var newVersion int
   130  	if len(versions) == 0 {
   131  		newVersion = 1
   132  	} else {
   133  		newVersion = versions[len(versions)-1] + 1
   134  	}
   135  	schemaJSON, err := schema.MarshalJSON()
   136  	if err != nil {
   137  		return err
   138  	}
   139  	cacheKey := subjectJSON{
   140  		subject: subject,
   141  		json:    string(schemaJSON),
   142  	}
   143  	c.versionCacheLock.Lock()
   144  	c.versionCache[cacheKey] = versionCacheEntry{newVersion, false}
   145  	c.versionCacheLock.Unlock()
   146  	return nil
   147  }
   148  
   149  // GetBySubjectAndID returns the schema identified by id
   150  // Returns Schema object on success
   151  func (c *mockclient) GetBySubjectAndID(subject string, id int) (schema SchemaInfo, err error) {
   152  	cacheKey := subjectID{
   153  		subject: subject,
   154  		id:      id,
   155  	}
   156  	c.idCacheLock.RLock()
   157  	info, ok := c.idCache[cacheKey]
   158  	c.idCacheLock.RUnlock()
   159  	if ok {
   160  		return *info, nil
   161  	}
   162  	posErr := url.Error{
   163  		Op:  "GET",
   164  		URL: c.url.String() + fmt.Sprintf(schemasBySubject, id, url.QueryEscape(subject)),
   165  		Err: errors.New("Subject Not Found"),
   166  	}
   167  	return SchemaInfo{}, &posErr
   168  }
   169  
   170  // GetID checks if a schema has been registered with the subject. Returns ID if the registration can be found
   171  func (c *mockclient) GetID(subject string, schema SchemaInfo, normalize bool) (id int, err error) {
   172  	schemaJSON, err := schema.MarshalJSON()
   173  	if err != nil {
   174  		return -1, err
   175  	}
   176  	cacheKey := subjectJSON{
   177  		subject: subject,
   178  		json:    string(schemaJSON),
   179  	}
   180  	c.schemaCacheLock.RLock()
   181  	idCacheEntryVal, ok := c.schemaCache[cacheKey]
   182  	if idCacheEntryVal.softDeleted {
   183  		ok = false
   184  	}
   185  	c.schemaCacheLock.RUnlock()
   186  	if ok {
   187  		return idCacheEntryVal.id, nil
   188  	}
   189  
   190  	posErr := url.Error{
   191  		Op:  "GET",
   192  		URL: c.url.String() + fmt.Sprintf(subjects, url.PathEscape(subject)),
   193  		Err: errors.New("Subject Not found"),
   194  	}
   195  	return -1, &posErr
   196  }
   197  
   198  // GetLatestSchemaMetadata fetches latest version registered with the provided subject
   199  // Returns SchemaMetadata object
   200  func (c *mockclient) GetLatestSchemaMetadata(subject string) (result SchemaMetadata, err error) {
   201  	version := c.latestVersion(subject)
   202  	if version < 0 {
   203  		posErr := url.Error{
   204  			Op:  "GET",
   205  			URL: c.url.String() + fmt.Sprintf(versions, url.PathEscape(subject), "latest"),
   206  			Err: errors.New("Subject Not found"),
   207  		}
   208  		return SchemaMetadata{}, &posErr
   209  	}
   210  	return c.GetSchemaMetadata(subject, version)
   211  }
   212  
   213  // GetSchemaMetadata fetches the requested subject schema identified by version
   214  // Returns SchemaMetadata object
   215  func (c *mockclient) GetSchemaMetadata(subject string, version int) (result SchemaMetadata, err error) {
   216  	var json string
   217  	c.versionCacheLock.RLock()
   218  	for key, value := range c.versionCache {
   219  		if key.subject == subject && value.version == version && !value.softDeleted {
   220  			json = key.json
   221  			break
   222  		}
   223  	}
   224  	c.versionCacheLock.RUnlock()
   225  	if json == "" {
   226  		posErr := url.Error{
   227  			Op:  "GET",
   228  			URL: c.url.String() + fmt.Sprintf(versions, url.PathEscape(subject), version),
   229  			Err: errors.New("Subject Not found"),
   230  		}
   231  		return SchemaMetadata{}, &posErr
   232  	}
   233  
   234  	var info SchemaInfo
   235  	err = info.UnmarshalJSON([]byte(json))
   236  	if err != nil {
   237  		return SchemaMetadata{}, err
   238  	}
   239  	var id = -1
   240  	c.idCacheLock.RLock()
   241  	for key, value := range c.idCache {
   242  		if key.subject == subject && schemasEqual(*value, info) {
   243  			id = key.id
   244  			break
   245  		}
   246  	}
   247  	c.idCacheLock.RUnlock()
   248  	if id == -1 {
   249  		posErr := url.Error{
   250  			Op:  "GET",
   251  			URL: c.url.String() + fmt.Sprintf(versions, url.PathEscape(subject), version),
   252  			Err: errors.New("Subject Not found"),
   253  		}
   254  		return SchemaMetadata{}, &posErr
   255  	}
   256  	return SchemaMetadata{
   257  		SchemaInfo: info,
   258  
   259  		ID:      id,
   260  		Subject: subject,
   261  		Version: version,
   262  	}, nil
   263  }
   264  
   265  // GetAllVersions fetches a list of all version numbers associated with the provided subject registration
   266  // Returns integer slice on success
   267  func (c *mockclient) GetAllVersions(subject string) (results []int, err error) {
   268  	results = c.allVersions(subject)
   269  	if len(results) == 0 {
   270  		posErr := url.Error{
   271  			Op:  "GET",
   272  			URL: c.url.String() + fmt.Sprintf(version, url.PathEscape(subject)),
   273  			Err: errors.New("Subject Not Found"),
   274  		}
   275  		return nil, &posErr
   276  	}
   277  	return results, err
   278  }
   279  
   280  func (c *mockclient) allVersions(subject string) (results []int) {
   281  	versions := make([]int, 0)
   282  	c.versionCacheLock.RLock()
   283  	for key, value := range c.versionCache {
   284  		if key.subject == subject && !value.softDeleted {
   285  			versions = append(versions, value.version)
   286  		}
   287  	}
   288  	c.versionCacheLock.RUnlock()
   289  	sort.Ints(versions)
   290  	return versions
   291  }
   292  
   293  func (c *mockclient) latestVersion(subject string) int {
   294  	versions := c.allVersions(subject)
   295  	if len(versions) == 0 {
   296  		return -1
   297  	}
   298  	return versions[len(versions)-1]
   299  }
   300  
   301  func (c *mockclient) deleteVersion(key subjectJSON, version int, permanent bool) {
   302  	if permanent {
   303  		delete(c.versionCache, key)
   304  	} else {
   305  		c.versionCache[key] = versionCacheEntry{version, true}
   306  	}
   307  }
   308  
   309  func (c *mockclient) deleteID(key subjectJSON, id int, permanent bool) {
   310  	if permanent {
   311  		delete(c.schemaCache, key)
   312  	} else {
   313  		c.schemaCache[key] = idCacheEntry{id, true}
   314  	}
   315  }
   316  
   317  // GetVersion finds the Subject SchemaMetadata associated with the provided schema
   318  // Returns integer SchemaMetadata number
   319  func (c *mockclient) GetVersion(subject string, schema SchemaInfo, normalize bool) (int, error) {
   320  	schemaJSON, err := schema.MarshalJSON()
   321  	if err != nil {
   322  		return -1, err
   323  	}
   324  	cacheKey := subjectJSON{
   325  		subject: subject,
   326  		json:    string(schemaJSON),
   327  	}
   328  	c.versionCacheLock.RLock()
   329  	versionCacheEntryVal, ok := c.versionCache[cacheKey]
   330  	if versionCacheEntryVal.softDeleted {
   331  		ok = false
   332  	}
   333  	c.versionCacheLock.RUnlock()
   334  	if ok {
   335  		return versionCacheEntryVal.version, nil
   336  	}
   337  	posErr := url.Error{
   338  		Op:  "GET",
   339  		URL: c.url.String() + fmt.Sprintf(subjects, url.PathEscape(subject)),
   340  		Err: errors.New("Subject Not Found"),
   341  	}
   342  	return -1, &posErr
   343  }
   344  
   345  // Fetch all Subjects registered with the schema Registry
   346  // Returns a string slice containing all registered subjects
   347  func (c *mockclient) GetAllSubjects() ([]string, error) {
   348  	subjects := make([]string, 0)
   349  	c.versionCacheLock.RLock()
   350  	for key, value := range c.versionCache {
   351  		if !value.softDeleted {
   352  			subjects = append(subjects, key.subject)
   353  		}
   354  	}
   355  	c.versionCacheLock.RUnlock()
   356  	sort.Strings(subjects)
   357  	return subjects, nil
   358  }
   359  
   360  // Deletes provided Subject from registry
   361  // Returns integer slice of versions removed by delete
   362  func (c *mockclient) DeleteSubject(subject string, permanent bool) (deleted []int, err error) {
   363  	c.schemaCacheLock.Lock()
   364  	for key, value := range c.schemaCache {
   365  		if key.subject == subject && (!value.softDeleted || permanent) {
   366  			c.deleteID(key, value.id, permanent)
   367  		}
   368  	}
   369  	c.schemaCacheLock.Unlock()
   370  	c.versionCacheLock.Lock()
   371  	for key, value := range c.versionCache {
   372  		if key.subject == subject && (!value.softDeleted || permanent) {
   373  			c.deleteVersion(key, value.version, permanent)
   374  			deleted = append(deleted, value.version)
   375  		}
   376  	}
   377  	c.versionCacheLock.Unlock()
   378  	c.compatibilityCacheLock.Lock()
   379  	delete(c.compatibilityCache, subject)
   380  	c.compatibilityCacheLock.Unlock()
   381  	if permanent {
   382  		c.idCacheLock.Lock()
   383  		for key := range c.idCache {
   384  			if key.subject == subject {
   385  				delete(c.idCache, key)
   386  			}
   387  		}
   388  		c.idCacheLock.Unlock()
   389  	}
   390  	return deleted, nil
   391  }
   392  
   393  // DeleteSubjectVersion removes the version identified by delete from the subject's registration
   394  // Returns integer id for the deleted version
   395  func (c *mockclient) DeleteSubjectVersion(subject string, version int, permanent bool) (deleted int, err error) {
   396  	c.versionCacheLock.Lock()
   397  	for key, value := range c.versionCache {
   398  		if key.subject == subject && value.version == version {
   399  			c.deleteVersion(key, value.version, permanent)
   400  			schemaJSON := key.json
   401  			cacheKeySchema := subjectJSON{
   402  				subject: subject,
   403  				json:    string(schemaJSON),
   404  			}
   405  			c.schemaCacheLock.Lock()
   406  			idSchemaEntryVal, ok := c.schemaCache[cacheKeySchema]
   407  			if ok {
   408  				c.deleteID(key, idSchemaEntryVal.id, permanent)
   409  			}
   410  			c.schemaCacheLock.Unlock()
   411  			if permanent && ok {
   412  				c.idCacheLock.Lock()
   413  				cacheKeyID := subjectID{
   414  					subject: subject,
   415  					id:      idSchemaEntryVal.id,
   416  				}
   417  				delete(c.idCache, cacheKeyID)
   418  				c.idCacheLock.Unlock()
   419  			}
   420  		}
   421  	}
   422  	c.versionCacheLock.Unlock()
   423  	return version, nil
   424  }
   425  
   426  // Fetch compatibility level currently configured for provided subject
   427  // Returns compatibility level string upon success
   428  func (c *mockclient) GetCompatibility(subject string) (compatibility Compatibility, err error) {
   429  	c.compatibilityCacheLock.RLock()
   430  	compatibility, ok := c.compatibilityCache[subject]
   431  	c.compatibilityCacheLock.RUnlock()
   432  	if !ok {
   433  		posErr := url.Error{
   434  			Op:  "GET",
   435  			URL: c.url.String() + fmt.Sprintf(subjectConfig, url.PathEscape(subject)),
   436  			Err: errors.New("Subject Not Found"),
   437  		}
   438  		return compatibility, &posErr
   439  	}
   440  	return compatibility, nil
   441  }
   442  
   443  // UpdateCompatibility updates subject's compatibility level
   444  // Returns new compatibility level string upon success
   445  func (c *mockclient) UpdateCompatibility(subject string, update Compatibility) (compatibility Compatibility, err error) {
   446  	c.compatibilityCacheLock.Lock()
   447  	c.compatibilityCache[subject] = update
   448  	c.compatibilityCacheLock.Unlock()
   449  	return update, nil
   450  }
   451  
   452  // TestCompatibility verifies schema against the subject's compatibility policy
   453  // Returns true if the schema is compatible, false otherwise
   454  func (c *mockclient) TestCompatibility(subject string, version int, schema SchemaInfo) (ok bool, err error) {
   455  	return false, errors.New("unsupported operaiton")
   456  }
   457  
   458  // GetDefaultCompatibility fetches the global(default) compatibility level
   459  // Returns global(default) compatibility level
   460  func (c *mockclient) GetDefaultCompatibility() (compatibility Compatibility, err error) {
   461  	c.compatibilityCacheLock.RLock()
   462  	compatibility, ok := c.compatibilityCache[noSubject]
   463  	c.compatibilityCacheLock.RUnlock()
   464  	if !ok {
   465  		posErr := url.Error{
   466  			Op:  "GET",
   467  			URL: c.url.String() + fmt.Sprintf(config),
   468  			Err: errors.New("Subject Not Found"),
   469  		}
   470  		return compatibility, &posErr
   471  	}
   472  	return compatibility, nil
   473  }
   474  
   475  // UpdateDefaultCompatibility updates the global(default) compatibility level level
   476  // Returns new string compatibility level
   477  func (c *mockclient) UpdateDefaultCompatibility(update Compatibility) (compatibility Compatibility, err error) {
   478  	c.compatibilityCacheLock.Lock()
   479  	c.compatibilityCache[noSubject] = update
   480  	c.compatibilityCacheLock.Unlock()
   481  	return update, nil
   482  }
   483  
   484  func schemasEqual(info1 SchemaInfo, info2 SchemaInfo) bool {
   485  	refs1 := info1.References
   486  	if refs1 == nil {
   487  		refs1 = make([]Reference, 0)
   488  	}
   489  	refs2 := info2.References
   490  	if refs2 == nil {
   491  		refs2 = make([]Reference, 0)
   492  	}
   493  	return info1.Schema == info2.Schema &&
   494  		info1.SchemaType == info2.SchemaType &&
   495  		reflect.DeepEqual(refs1, refs2)
   496  }