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 }