You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In the official document with BACKWARD compatibility, we can add an optional field to an old schema, which is ignored by default value,
here is my code for the client to check schema compatibility:
client.go
package compatibility
import (
"bytes""context""fmt""github.com/hamba/avro/v2""github.com/hamba/avro/v2/registry"
jsoniter "github.com/json-iterator/go""io""net""net/http""net/url""path""strconv""strings""time"
)
// credentials are used to store the basic auth credentials.typecredentialsstruct {
usernamestringpasswordstring
}
// Client is a client for the schema registry.typeClientstruct {
client*http.Clientbase*url.URLcredscredentials
}
// Error is returned by the registry when there is an error.typeErrorstruct {
StatusCodeint`json:"-"`Codeint`json:"error_code"`Messagestring`json:"message"`
}
typeClientFuncfunc(*Client)
// Request is the request to test compatibility.typeRequeststruct {
Schemastring`json:"schema"`References []registry.SchemaReference`json:"references"`
}
// Response is the response from the compatibility check.typeResponsestruct {
IsCompatiblebool`json:"is_compatible"`Messages []string`json:"messages"`
}
// defaultClient is the default HTTP client.vardefaultClient=&http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 15*time.Second,
KeepAlive: 90*time.Second,
}).DialContext,
TLSHandshakeTimeout: 3*time.Second,
IdleConnTimeout: 90*time.Second,
},
Timeout: 10*time.Second,
}
constcontentType="application/vnd.schemaregistry.v1+json"// WithBasicAuth sets the basic auth credentials for the client.funcWithBasicAuth(username, passwordstring) ClientFunc {
returnfunc(c*Client) {
c.creds=credentials{username: username, password: password}
}
}
// WithHTTPClient sets the HTTP client for the client.funcWithHTTPClient(client*http.Client) ClientFunc {
returnfunc(c*Client) {
c.client=client
}
}
// NewClient creates a new schema registry client.funcNewClient(baseURLstring, opts...ClientFunc) (*Client, error) {
u, err:=url.Parse(baseURL)
iferr!=nil {
returnnil, err
}
if!strings.HasSuffix(u.Path, "/") {
u.Path+="/"
}
c:=&Client{
client: defaultClient,
base: u,
}
for_, opt:=rangeopts {
opt(c)
}
returnc, nil
}
// request performs a request to the schema registry.func (c*Client) request(ctx context.Context, method, pathstring, in, outany) error {
varbody io.Readerifin!=nil {
b, _:=jsoniter.Marshal(in)
body=bytes.NewReader(b)
}
// These errors are not possible as we have already parse the base URL.u, _:=c.base.Parse(path)
req, _:=http.NewRequestWithContext(ctx, method, u.String(), body)
req.Header.Set("Content-Type", contentType)
iflen(c.creds.username) >0||len(c.creds.password) >0 {
req.SetBasicAuth(c.creds.username, c.creds.password)
}
resp, err:=c.client.Do(req)
iferr!=nil {
returnfmt.Errorf("could not perform request: %w", err)
}
deferfunc() {
_, _=io.Copy(io.Discard, resp.Body)
_=resp.Body.Close()
}()
ifresp.StatusCode>=http.StatusBadRequest {
err:=Error{StatusCode: resp.StatusCode}
_=jsoniter.NewDecoder(resp.Body).Decode(&err)
returnerr
}
ifout!=nil {
returnjsoniter.NewDecoder(resp.Body).Decode(out)
}
returnnil
}
// TestCompatibility tests the compatibility of a schema with the schema registry.//// - The schema is tested against the subject and version provided.//// - If the schema is compatible with the subject and version, the response will be true.//// - If the schema is not compatible with the subject and version, the response will be false// and the reason will be provided in the messages.func (c*Client) TestCompatibility(
ctx context.Context,
subject,
versionstring,
schema avro.Schema,
references []registry.SchemaReference,
verbosebool,
) (responseResponse, errerror) {
p:=path.Join("compatibility", "subjects", subject, "versions", version)
ifverbose {
p+="?verbose=true"
}
requestBody:=Request{
Schema: schema.String(),
References: references,
}
iferr:=c.request(ctx, http.MethodPost, p, requestBody, &response); err!=nil {
returnresponse, err
}
returnresponse, nil
}
// Error returns the error message.func (eError) Error() string {
ife.Message!="" {
returne.Message
}
return"registry error: "+strconv.Itoa(e.StatusCode)
}
package compatibility
import (
"context""github.com/brianvoe/gofakeit/v6""github.com/hamba/avro/v2""github.com/hamba/avro/v2/registry"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega""source.vtvlive.vn/cellutions/cases"
)
var_=Describe("Client", Ordered, func() {
varsubjectstringvarreferences []registry.SchemaReferenceDescribe("TestCompatibility", func() {
When("the old schema does not use references", func() {
BeforeAll(func() {
subject=cases.ToKebab(gofakeit.LoremIpsumSentence(4))
references=make([]registry.SchemaReference, 0)
oldSchema:=recordSchema(
"seenow.dev.temp",
` { "type": "record", "name": "SimpleRecord", "fields": [ { "name": "name", "type": "string" } ] }`)
_, _, err:=registryClient.CreateSchema(context.Background(), subject, oldSchema.String())
Expect(err).ToNot(HaveOccurred())
})
DescribeTable("should works",
func(newSchema avro.Schema, isCompatiblebool) {
ctx:=context.Background()
res, err:=compatibilityClient.TestCompatibility(ctx, subject, "latest", newSchema, references, true)
Expect(err).ToNot(HaveOccurred())
Expect(res.IsCompatible).To(Equal(isCompatible))
},
Entry("add an optional field to a record",
recordSchema("seenow.dev.temp",
` { "type": "record", "name": "SimpleRecord", "fields": [ { "name": "name", "type": "string" }, { "name": "age", "type": ["int", "null"], "default": 0 } ] }`), false,
),
Entry("delete a field from a record", Skip),
Entry("add non-optional field to a record", Skip),
)
})
When("the old schema is a union referencing records", func() {
DescribeTable("should works",
func() {
},
Entry("add an optional field to a record", Skip),
Entry("delete a field from a record", Skip),
Entry("add non-optional field to a record", Skip),
)
})
})
})
funcrecordSchema(namespace, schemaStringstring) *avro.RecordSchema {
GinkgoHelper()
deferGinkgoRecover()
schema, err:=avro.ParseWithCache(schemaString, namespace, avro.DefaultSchemaCache)
Expect(err).ToNot(HaveOccurred())
returnschema.(*avro.RecordSchema)
}
Im expect IsCompatible is return to true but the value is false and the error message is:
0 = {string} "{errorType:'READER_FIELD_MISSING_DEFAULT_VALUE', description:'The field 'age' at path '/fields/1' in the new schema has no default value and is missing in the old schema', additionalInfo:'age'}"
1 = {string} "{oldSchemaVersion: 1}"
2 = {string} "{oldSchema: '{"type":"record","name":"SimpleRecord","namespace":"seenow.dev.temp","fields":[{"name":"name","type":"string"}]}'}"
3 = {string} "{compatibility: 'BACKWARD'}"
Am I wrong?
The text was updated successfully, but these errors were encountered:
In the official document with BACKWARD compatibility, we can add an optional field to an old schema, which is ignored by default value,
here is my code for the client to check schema compatibility:
Im expect IsCompatible is return to
true
but the value isfalse
and the error message is:Am I wrong?
The text was updated successfully, but these errors were encountered: