schema_registry_client 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/lint.yml +18 -0
- data/.github/workflows/release.yml +31 -0
- data/.github/workflows/test.yml +22 -0
- data/.gitignore +1 -0
- data/.rspec +2 -0
- data/.rubocop.yml +39 -0
- data/CHANGELOG.md +12 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +150 -0
- data/LICENSE +20 -0
- data/README.md +48 -0
- data/Rakefile +3 -0
- data/lib/schema_registry_client/avro_schema_store.rb +127 -0
- data/lib/schema_registry_client/cached_confluent_schema_registry.rb +57 -0
- data/lib/schema_registry_client/confluent_schema_registry.rb +118 -0
- data/lib/schema_registry_client/output/json_schema.rb +78 -0
- data/lib/schema_registry_client/output/proto_text.rb +320 -0
- data/lib/schema_registry_client/schema/avro.rb +61 -0
- data/lib/schema_registry_client/schema/base.rb +44 -0
- data/lib/schema_registry_client/schema/proto_json_schema.rb +30 -0
- data/lib/schema_registry_client/schema/protobuf.rb +131 -0
- data/lib/schema_registry_client/version.rb +5 -0
- data/lib/schema_registry_client/wire.rb +30 -0
- data/lib/schema_registry_client.rb +156 -0
- data/schema_registry_client.gemspec +33 -0
- data/spec/decoding_spec.rb +183 -0
- data/spec/encoding_spec.rb +207 -0
- data/spec/gen/everything/everything_pb.rb +26 -0
- data/spec/gen/referenced/referer_pb.rb +24 -0
- data/spec/gen/simple/simple_pb.rb +18 -0
- data/spec/json_schema_spec.rb +12 -0
- data/spec/proto_text_spec.rb +10 -0
- data/spec/schemas/everything/everything.json +328 -0
- data/spec/schemas/everything/everything.proto +105 -0
- data/spec/schemas/referenced/referenced.json +16 -0
- data/spec/schemas/referenced/referer.proto +28 -0
- data/spec/schemas/referenced/v1/MessageBA.avsc +21 -0
- data/spec/schemas/simple/simple.json +12 -0
- data/spec/schemas/simple/simple.proto +12 -0
- data/spec/schemas/simple/v1/SimpleMessage.avsc +11 -0
- data/spec/spec_helper.rb +16 -0
- metadata +46 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5bd3f6c9d59bc29bfbcc32577761bd90b17282dc63b7423a1d74978235b316c8
|
|
4
|
+
data.tar.gz: fde47855e0974f734724d0c9fbe36b80644be5ed2f42215cbfa389e4951affdb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dd97621d317f2f951c960b2f928832004c8d38b8c4f0ebdf291bbdb68ed7d873f83f4a789426479ead787f77f1dc00c2a5c1898407373a1fdc27b448c6ed1f97
|
|
7
|
+
data.tar.gz: fe7f21af487e6e8294977e5b50e77bd2a77648df7eb1b9bd4cb56a709fe09e0401ecba9e44ba35854b6383d35fcb2baf3530cf485963f7887a4e1fa8c0fc6dc4
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: Lint
|
|
2
|
+
|
|
3
|
+
on: [push, pull_request]
|
|
4
|
+
|
|
5
|
+
jobs:
|
|
6
|
+
build:
|
|
7
|
+
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v3
|
|
12
|
+
- name: Set up Ruby ${{ matrix.ruby }}
|
|
13
|
+
uses: ruby/setup-ruby@v1
|
|
14
|
+
with:
|
|
15
|
+
ruby-version: 3.4
|
|
16
|
+
bundler-cache: true
|
|
17
|
+
- name: Run standardrb
|
|
18
|
+
run: bundle exec standardrb
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Release Gem
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches:
|
|
5
|
+
- main
|
|
6
|
+
tags:
|
|
7
|
+
- 'v*.*.*' # Matches semantic versioning tags like v1.0.0
|
|
8
|
+
workflow_dispatch: # Allows manual triggering of the workflow
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
push:
|
|
12
|
+
name: Push gem to RubyGems.org
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
|
17
|
+
contents: write # IMPORTANT: this permission is required for `rake release` to push the release tag
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
# Set up
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
with:
|
|
23
|
+
persist-credentials: false
|
|
24
|
+
- name: Set up Ruby
|
|
25
|
+
uses: ruby/setup-ruby@v1
|
|
26
|
+
with:
|
|
27
|
+
bundler-cache: true
|
|
28
|
+
ruby-version: 3.4
|
|
29
|
+
|
|
30
|
+
# Release
|
|
31
|
+
- uses: rubygems/release-gem@v1
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on: [push, pull_request]
|
|
4
|
+
|
|
5
|
+
jobs:
|
|
6
|
+
build:
|
|
7
|
+
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
strategy:
|
|
10
|
+
fail-fast: false
|
|
11
|
+
matrix:
|
|
12
|
+
ruby: [3.1, 3.2, 3.3, 3.4]
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v3
|
|
16
|
+
- name: Set up Ruby ${{ matrix.ruby }}
|
|
17
|
+
uses: ruby/setup-ruby@v1
|
|
18
|
+
with:
|
|
19
|
+
ruby-version: ${{ matrix.ruby }}
|
|
20
|
+
bundler-cache: true
|
|
21
|
+
- name: Build and test with RSpec
|
|
22
|
+
run: bundle exec rspec
|
data/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
coverage
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
Exclude:
|
|
3
|
+
- app/gen/**/*
|
|
4
|
+
- spec/gen/**/*
|
|
5
|
+
|
|
6
|
+
Metrics/MethodLength:
|
|
7
|
+
Severity: refactor
|
|
8
|
+
Max: 60
|
|
9
|
+
|
|
10
|
+
Metrics/ModuleLength:
|
|
11
|
+
Severity: refactor
|
|
12
|
+
Max: 800
|
|
13
|
+
|
|
14
|
+
Metrics/CyclomaticComplexity:
|
|
15
|
+
Severity: refactor
|
|
16
|
+
Max: 25
|
|
17
|
+
|
|
18
|
+
Metrics/ClassLength:
|
|
19
|
+
Severity: refactor
|
|
20
|
+
Max: 800
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
Metrics/BlockLength:
|
|
24
|
+
Enabled: false
|
|
25
|
+
|
|
26
|
+
Metrics/AbcSize:
|
|
27
|
+
Severity: refactor
|
|
28
|
+
Max: 60
|
|
29
|
+
|
|
30
|
+
Metrics/PerceivedComplexity:
|
|
31
|
+
Severity: refactor
|
|
32
|
+
Max: 15
|
|
33
|
+
|
|
34
|
+
Style/Documentation:
|
|
35
|
+
Enabled: false
|
|
36
|
+
|
|
37
|
+
Lint/UnusedMethodArgument:
|
|
38
|
+
AllowUnusedKeywordArguments: true
|
|
39
|
+
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## UNRELEASED
|
|
9
|
+
|
|
10
|
+
# 0.0.6 - 2026-01-02
|
|
11
|
+
|
|
12
|
+
* Initial release.
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
schema_registry_client (0.0.6)
|
|
5
|
+
avro
|
|
6
|
+
excon
|
|
7
|
+
google-protobuf
|
|
8
|
+
|
|
9
|
+
GEM
|
|
10
|
+
remote: https://rubygems.org/
|
|
11
|
+
specs:
|
|
12
|
+
addressable (2.8.8)
|
|
13
|
+
public_suffix (>= 2.0.2, < 8.0)
|
|
14
|
+
ast (2.4.3)
|
|
15
|
+
avro (1.12.1)
|
|
16
|
+
multi_json (~> 1.0)
|
|
17
|
+
bigdecimal (4.0.1)
|
|
18
|
+
crack (1.0.1)
|
|
19
|
+
bigdecimal
|
|
20
|
+
rexml
|
|
21
|
+
diff-lcs (1.6.2)
|
|
22
|
+
docile (1.4.1)
|
|
23
|
+
excon (1.3.2)
|
|
24
|
+
logger
|
|
25
|
+
google-protobuf (4.33.2)
|
|
26
|
+
bigdecimal
|
|
27
|
+
rake (>= 13)
|
|
28
|
+
google-protobuf (4.33.2-aarch64-linux-gnu)
|
|
29
|
+
bigdecimal
|
|
30
|
+
rake (>= 13)
|
|
31
|
+
google-protobuf (4.33.2-aarch64-linux-musl)
|
|
32
|
+
bigdecimal
|
|
33
|
+
rake (>= 13)
|
|
34
|
+
google-protobuf (4.33.2-arm64-darwin)
|
|
35
|
+
bigdecimal
|
|
36
|
+
rake (>= 13)
|
|
37
|
+
google-protobuf (4.33.2-x86-linux-gnu)
|
|
38
|
+
bigdecimal
|
|
39
|
+
rake (>= 13)
|
|
40
|
+
google-protobuf (4.33.2-x86-linux-musl)
|
|
41
|
+
bigdecimal
|
|
42
|
+
rake (>= 13)
|
|
43
|
+
google-protobuf (4.33.2-x86_64-darwin)
|
|
44
|
+
bigdecimal
|
|
45
|
+
rake (>= 13)
|
|
46
|
+
google-protobuf (4.33.2-x86_64-linux-gnu)
|
|
47
|
+
bigdecimal
|
|
48
|
+
rake (>= 13)
|
|
49
|
+
google-protobuf (4.33.2-x86_64-linux-musl)
|
|
50
|
+
bigdecimal
|
|
51
|
+
rake (>= 13)
|
|
52
|
+
hashdiff (1.2.1)
|
|
53
|
+
json (2.18.0)
|
|
54
|
+
language_server-protocol (3.17.0.5)
|
|
55
|
+
lint_roller (1.1.0)
|
|
56
|
+
logger (1.7.0)
|
|
57
|
+
multi_json (1.19.1)
|
|
58
|
+
parallel (1.27.0)
|
|
59
|
+
parser (3.3.10.0)
|
|
60
|
+
ast (~> 2.4.1)
|
|
61
|
+
racc
|
|
62
|
+
prism (1.7.0)
|
|
63
|
+
public_suffix (7.0.0)
|
|
64
|
+
racc (1.8.1)
|
|
65
|
+
rainbow (3.1.1)
|
|
66
|
+
rake (13.3.1)
|
|
67
|
+
regexp_parser (2.11.3)
|
|
68
|
+
rexml (3.4.4)
|
|
69
|
+
rspec (3.13.2)
|
|
70
|
+
rspec-core (~> 3.13.0)
|
|
71
|
+
rspec-expectations (~> 3.13.0)
|
|
72
|
+
rspec-mocks (~> 3.13.0)
|
|
73
|
+
rspec-core (3.13.6)
|
|
74
|
+
rspec-support (~> 3.13.0)
|
|
75
|
+
rspec-expectations (3.13.5)
|
|
76
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
77
|
+
rspec-support (~> 3.13.0)
|
|
78
|
+
rspec-mocks (3.13.7)
|
|
79
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
80
|
+
rspec-support (~> 3.13.0)
|
|
81
|
+
rspec-support (3.13.6)
|
|
82
|
+
rubocop (1.81.7)
|
|
83
|
+
json (~> 2.3)
|
|
84
|
+
language_server-protocol (~> 3.17.0.2)
|
|
85
|
+
lint_roller (~> 1.1.0)
|
|
86
|
+
parallel (~> 1.10)
|
|
87
|
+
parser (>= 3.3.0.2)
|
|
88
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
89
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
|
90
|
+
rubocop-ast (>= 1.47.1, < 2.0)
|
|
91
|
+
ruby-progressbar (~> 1.7)
|
|
92
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
|
93
|
+
rubocop-ast (1.49.0)
|
|
94
|
+
parser (>= 3.3.7.2)
|
|
95
|
+
prism (~> 1.7)
|
|
96
|
+
rubocop-performance (1.26.1)
|
|
97
|
+
lint_roller (~> 1.1)
|
|
98
|
+
rubocop (>= 1.75.0, < 2.0)
|
|
99
|
+
rubocop-ast (>= 1.47.1, < 2.0)
|
|
100
|
+
ruby-progressbar (1.13.0)
|
|
101
|
+
simplecov (0.22.0)
|
|
102
|
+
docile (~> 1.1)
|
|
103
|
+
simplecov-html (~> 0.11)
|
|
104
|
+
simplecov_json_formatter (~> 0.1)
|
|
105
|
+
simplecov-html (0.13.2)
|
|
106
|
+
simplecov_json_formatter (0.1.4)
|
|
107
|
+
standard (1.52.0)
|
|
108
|
+
language_server-protocol (~> 3.17.0.2)
|
|
109
|
+
lint_roller (~> 1.0)
|
|
110
|
+
rubocop (~> 1.81.7)
|
|
111
|
+
standard-custom (~> 1.0.0)
|
|
112
|
+
standard-performance (~> 1.8)
|
|
113
|
+
standard-custom (1.0.2)
|
|
114
|
+
lint_roller (~> 1.0)
|
|
115
|
+
rubocop (~> 1.50)
|
|
116
|
+
standard-performance (1.9.0)
|
|
117
|
+
lint_roller (~> 1.1)
|
|
118
|
+
rubocop-performance (~> 1.26.0)
|
|
119
|
+
standardrb (1.0.1)
|
|
120
|
+
standard
|
|
121
|
+
unicode-display_width (3.2.0)
|
|
122
|
+
unicode-emoji (~> 4.1)
|
|
123
|
+
unicode-emoji (4.2.0)
|
|
124
|
+
webmock (3.26.1)
|
|
125
|
+
addressable (>= 2.8.0)
|
|
126
|
+
crack (>= 0.3.2)
|
|
127
|
+
hashdiff (>= 0.4.0, < 2.0.0)
|
|
128
|
+
|
|
129
|
+
PLATFORMS
|
|
130
|
+
aarch64-linux-gnu
|
|
131
|
+
aarch64-linux-musl
|
|
132
|
+
arm64-darwin
|
|
133
|
+
ruby
|
|
134
|
+
x86-linux-gnu
|
|
135
|
+
x86-linux-musl
|
|
136
|
+
x86_64-darwin
|
|
137
|
+
x86_64-linux-gnu
|
|
138
|
+
x86_64-linux-musl
|
|
139
|
+
|
|
140
|
+
DEPENDENCIES
|
|
141
|
+
bundler (~> 2.0)
|
|
142
|
+
rake (~> 13.0)
|
|
143
|
+
rspec (~> 3.2)
|
|
144
|
+
schema_registry_client!
|
|
145
|
+
simplecov
|
|
146
|
+
standardrb
|
|
147
|
+
webmock
|
|
148
|
+
|
|
149
|
+
BUNDLED WITH
|
|
150
|
+
2.6.9
|
data/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# schema_registry_client
|
|
2
|
+
|
|
3
|
+
`schema_registry_client` is a library to interact with the Confluent Schema Registry using Google Protobuf. It is inspired by and based off of [avro_turf](https://github.com/dasch/avro_turf).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'schema_registry_client'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
And then execute:
|
|
14
|
+
|
|
15
|
+
$ bundle
|
|
16
|
+
|
|
17
|
+
Or install it yourself as:
|
|
18
|
+
|
|
19
|
+
$ gem install schema_registry_client
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
SchemaRegistry interacts with the Confluent Schema Registry, and caches all results. When you first encode a message, it will register the message and all dependencies with the Schema Registry. When decoding, it will look up the schema in the Schema Registry and use the associated local generated code to decode the message.
|
|
24
|
+
|
|
25
|
+
Example usage:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
require 'schema_registry_client'
|
|
29
|
+
|
|
30
|
+
schema_registry_client = SchemaRegistry.new(registry_url: 'http://localhost:8081', schema_paths: ['path/to/protos'])
|
|
31
|
+
message = MyProto::MyMessage.new(field1: 'value1', field2: 42)
|
|
32
|
+
encoded = schema_registry_client.encode(message, subject: 'my-subject')
|
|
33
|
+
|
|
34
|
+
# Decoding
|
|
35
|
+
|
|
36
|
+
decoded_proto_message = schema_registry_client.decode(encoded_string)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Notes about usage
|
|
40
|
+
|
|
41
|
+
* When decoding, this library does *not* attempt to fully parse the Proto definition stored on the schema registry and generate dynamic classes. Instead, it simply parses out the package and message and assumes that the reader has the message available in the descriptor pool. Any compatibility issues should be detected through normal means, i.e. just by instantiating the message and seeing if any errors are raised.
|
|
42
|
+
|
|
43
|
+
### Regenerating test protos
|
|
44
|
+
Run the following to regenerate:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
protoc -I spec/schemas --ruby_out=spec/gen --ruby_opt=paths=source_relative spec/schemas/**/*.proto
|
|
48
|
+
```
|
data/Rakefile
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'avro'
|
|
4
|
+
|
|
5
|
+
class SchemaRegistry
|
|
6
|
+
class AvroSchemaStore
|
|
7
|
+
def initialize(path: nil)
|
|
8
|
+
@path = path or raise 'Please specify a schema path'
|
|
9
|
+
@schemas = {}
|
|
10
|
+
@schema_text = {}
|
|
11
|
+
@mutex = Mutex.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
attr_accessor :schemas
|
|
15
|
+
|
|
16
|
+
def find_text(name)
|
|
17
|
+
@schema_text[name]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Resolves and returns a schema.
|
|
21
|
+
#
|
|
22
|
+
# schema_name - The String name of the schema to resolve.
|
|
23
|
+
#
|
|
24
|
+
# Returns an Avro::Schema.
|
|
25
|
+
def find(name)
|
|
26
|
+
# Optimistic non-blocking read from @schemas
|
|
27
|
+
# No sense to lock the resource when all the schemas already loaded
|
|
28
|
+
return @schemas[name] if @schemas.key?(name)
|
|
29
|
+
|
|
30
|
+
# Pessimistic blocking write to @schemas
|
|
31
|
+
@mutex.synchronize do
|
|
32
|
+
# Still need to check is the schema already loaded
|
|
33
|
+
return @schemas[name] if @schemas.key?(name)
|
|
34
|
+
|
|
35
|
+
load_schema!(name, @schemas.dup)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Loads all schema definition files in the `schemas_dir`.
|
|
40
|
+
def load_schemas!
|
|
41
|
+
pattern = [@path, '**', '*.avsc'].join('/')
|
|
42
|
+
|
|
43
|
+
Dir.glob(pattern) do |schema_path|
|
|
44
|
+
# Remove the path prefix.
|
|
45
|
+
schema_path.sub!(%r{^/?#{@path}/}, '')
|
|
46
|
+
|
|
47
|
+
# Replace `/` with `.` and chop off the file extension.
|
|
48
|
+
schema_name = File.basename(schema_path.tr('/', '.'), '.avsc')
|
|
49
|
+
|
|
50
|
+
# Load and cache the schema.
|
|
51
|
+
find(schema_name)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @param schema_hash [Hash]
|
|
56
|
+
def add_schema(schema_hash)
|
|
57
|
+
name = schema_hash['name']
|
|
58
|
+
namespace = schema_hash['namespace']
|
|
59
|
+
full_name = Avro::Name.make_fullname(name, namespace)
|
|
60
|
+
return if @schemas.key?(full_name)
|
|
61
|
+
|
|
62
|
+
# We pass in copy of @schemas which Avro can freely modify
|
|
63
|
+
# and register the sub-schema. It doesn't matter because
|
|
64
|
+
# we will discard it.
|
|
65
|
+
schema = Avro::Schema.real_parse(schema_hash, @schemas.dup)
|
|
66
|
+
@schemas[full_name] = schema
|
|
67
|
+
@schema_text[full_name] = JSON.pretty_generate(schema_hash)
|
|
68
|
+
|
|
69
|
+
schema
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
protected
|
|
73
|
+
|
|
74
|
+
# Loads single schema
|
|
75
|
+
# Such method is not thread-safe, do not call it of from mutex synchronization routine
|
|
76
|
+
def load_schema!(fullname, local_schemas_cache = {})
|
|
77
|
+
schema_path = build_schema_path(fullname)
|
|
78
|
+
schema_text = File.read(schema_path)
|
|
79
|
+
schema_json = JSON.parse(schema_text)
|
|
80
|
+
|
|
81
|
+
schema = Avro::Schema.real_parse(schema_json, local_schemas_cache)
|
|
82
|
+
|
|
83
|
+
# Don't cache the parsed schema until after its fullname is validated
|
|
84
|
+
if schema.respond_to?(:fullname) && schema.fullname != fullname
|
|
85
|
+
raise SchemaRegistry::SchemaError, "expected schema `#{schema_path}' to define type `#{fullname}'"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Cache only this new top-level schema by its fullname. It's critical
|
|
89
|
+
# not to make every sub-schema resolvable at the top level here because
|
|
90
|
+
# multiple different avsc files may define the same sub-schema, and
|
|
91
|
+
# if we share the @schemas cache across all parsing contexts, the Avro
|
|
92
|
+
# gem will raise an Avro::SchemaParseError when parsing another avsc
|
|
93
|
+
# file that contains a subschema with the same fullname as one
|
|
94
|
+
# encountered previously in a different file:
|
|
95
|
+
# <Avro::SchemaParseError: The name "foo.bar" is already in use.>
|
|
96
|
+
# Essentially, the only schemas that should be resolvable in @schemas
|
|
97
|
+
# are those that have their own .avsc files on disk.
|
|
98
|
+
@schemas[fullname] = schema
|
|
99
|
+
@schema_text[fullname] = schema_text
|
|
100
|
+
|
|
101
|
+
schema
|
|
102
|
+
rescue ::Avro::UnknownSchemaError => e
|
|
103
|
+
# Try to first resolve a referenced schema from disk.
|
|
104
|
+
# If this is successful, the Avro gem will have mutated the
|
|
105
|
+
# local_schemas_cache, adding all the new schemas it found.
|
|
106
|
+
load_schema!(::Avro::Name.make_fullname(e.type_name, e.default_namespace), local_schemas_cache)
|
|
107
|
+
|
|
108
|
+
# Attempt to re-parse the original schema now that the dependency
|
|
109
|
+
# has been resolved and use the now-updated local_schemas_cache to
|
|
110
|
+
# pick up where we left off.
|
|
111
|
+
local_schemas_cache.delete(fullname)
|
|
112
|
+
# Ensure all sub-schemas are cleaned up to avoid conflicts when re-parsing
|
|
113
|
+
# schema.
|
|
114
|
+
local_schemas_cache.each_key do |schema_name|
|
|
115
|
+
local_schemas_cache.delete(schema_name) unless File.exist?(build_schema_path(schema_name))
|
|
116
|
+
end
|
|
117
|
+
load_schema!(fullname, @schemas.dup)
|
|
118
|
+
rescue Errno::ENOENT, Errno::ENAMETOOLONG
|
|
119
|
+
raise "could not find Avro schema at `#{schema_path}'"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def build_schema_path(fullname)
|
|
123
|
+
*namespace, schema_name = fullname.split('.')
|
|
124
|
+
File.join(@path, *namespace, "#{schema_name}.avsc")
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class SchemaRegistry
|
|
4
|
+
class CachedConfluentSchemaRegistry
|
|
5
|
+
# @param upstream [SchemaRegistry::ConfluentSchemaRegistry]
|
|
6
|
+
def initialize(upstream)
|
|
7
|
+
@upstream = upstream
|
|
8
|
+
@schemas_by_id = {}
|
|
9
|
+
@ids_by_schema = {}
|
|
10
|
+
@versions_by_subject_and_id = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Delegate the following methods to the upstream
|
|
14
|
+
%i[subject_versions schema_subject_versions].each do |name|
|
|
15
|
+
define_method(name) do |*args|
|
|
16
|
+
instance_variable_get(:@upstream).send(name, *args)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param id [Integer] the schema ID to fetch
|
|
21
|
+
# @return [String] the schema string stored in the registry for the given id
|
|
22
|
+
def fetch(id)
|
|
23
|
+
@schemas_by_id[id] ||= @upstream.fetch(id)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param id [Integer] the schema ID to fetch
|
|
27
|
+
# @param subject [String] the subject to fetch the version for
|
|
28
|
+
# @return [Integer, nil] the version of the schema for the given subject and id, or nil if not found
|
|
29
|
+
def fetch_version(id, subject)
|
|
30
|
+
key = [subject, id]
|
|
31
|
+
return @versions_by_subject_and_id[key] if @versions_by_subject_and_id[key]
|
|
32
|
+
|
|
33
|
+
results = @upstream.schema_subject_versions(id)
|
|
34
|
+
@versions_by_subject_and_id[key] = results&.find { |r| r['subject'] == subject }&.dig('version')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param subject [String] the subject to check
|
|
38
|
+
# @param schema [String] the schema text to check
|
|
39
|
+
# @return [Boolean] true if we know the schema has been registered for that subject.
|
|
40
|
+
def registered?(subject, schema)
|
|
41
|
+
@ids_by_schema[[subject, schema]] && !@ids_by_schema[[subject, schema]].empty?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @param subject [String] the subject to register the schema under
|
|
45
|
+
# @param schema [String] the schema text to register
|
|
46
|
+
# @param references [Array<Hash>] optional references to other schemas
|
|
47
|
+
# @param schema_type [String]
|
|
48
|
+
def register(subject, schema, references: [], schema_type: 'PROTOBUF')
|
|
49
|
+
key = [subject, schema]
|
|
50
|
+
|
|
51
|
+
@ids_by_schema[key] ||= @upstream.register(subject,
|
|
52
|
+
schema,
|
|
53
|
+
references: references,
|
|
54
|
+
schema_type: schema_type)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'excon'
|
|
4
|
+
|
|
5
|
+
class SchemaRegistry
|
|
6
|
+
class ConfluentSchemaRegistry
|
|
7
|
+
CONTENT_TYPE = 'application/vnd.schemaregistry.v1+json'
|
|
8
|
+
|
|
9
|
+
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
10
|
+
url,
|
|
11
|
+
schema_context: nil,
|
|
12
|
+
logger: Logger.new($stdout),
|
|
13
|
+
proxy: nil,
|
|
14
|
+
user: nil,
|
|
15
|
+
password: nil,
|
|
16
|
+
ssl_ca_file: nil,
|
|
17
|
+
client_cert: nil,
|
|
18
|
+
client_key: nil,
|
|
19
|
+
client_key_pass: nil,
|
|
20
|
+
client_cert_data: nil,
|
|
21
|
+
client_key_data: nil,
|
|
22
|
+
path_prefix: nil,
|
|
23
|
+
connect_timeout: nil,
|
|
24
|
+
resolv_resolver: nil,
|
|
25
|
+
retry_limit: nil
|
|
26
|
+
)
|
|
27
|
+
@path_prefix = path_prefix
|
|
28
|
+
@schema_context_prefix = schema_context.nil? ? '' : ":.#{schema_context}:"
|
|
29
|
+
@schema_context_options = schema_context.nil? ? {} : { query: { subject: @schema_context_prefix } }
|
|
30
|
+
@logger = logger
|
|
31
|
+
headers = Excon.defaults[:headers].merge(
|
|
32
|
+
'Content-Type' => CONTENT_TYPE
|
|
33
|
+
)
|
|
34
|
+
params = {
|
|
35
|
+
headers: headers,
|
|
36
|
+
user: user,
|
|
37
|
+
password: password,
|
|
38
|
+
proxy: proxy,
|
|
39
|
+
ssl_ca_file: ssl_ca_file,
|
|
40
|
+
client_cert: client_cert,
|
|
41
|
+
client_key: client_key,
|
|
42
|
+
client_key_pass: client_key_pass,
|
|
43
|
+
client_cert_data: client_cert_data,
|
|
44
|
+
client_key_data: client_key_data,
|
|
45
|
+
resolv_resolver: resolv_resolver,
|
|
46
|
+
connect_timeout: connect_timeout,
|
|
47
|
+
retry_limit: retry_limit
|
|
48
|
+
}
|
|
49
|
+
# Remove nil params to allow Excon to use its default values
|
|
50
|
+
params.reject! { |_, v| v.nil? }
|
|
51
|
+
@connection = Excon.new(
|
|
52
|
+
url,
|
|
53
|
+
params
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @param id [Integer] the schema ID to fetch
|
|
58
|
+
# @return [String] the schema string stored in the registry for the given id
|
|
59
|
+
def fetch(id)
|
|
60
|
+
@logger.info "Fetching schema with id #{id}"
|
|
61
|
+
data = get("/schemas/ids/#{id}", idempotent: true, **@schema_context_options)
|
|
62
|
+
data.fetch('schema')
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @param schema_id [Integer] the schema ID to fetch versions for
|
|
66
|
+
# @return [Array<Hash>] an array of versions for the given schema ID
|
|
67
|
+
def schema_subject_versions(schema_id)
|
|
68
|
+
get("/schemas/ids/#{schema_id}/versions", idempotent: true, **@schema_context_options)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @param subject [String] the subject to check
|
|
72
|
+
# @param schema [String] the schema text to check
|
|
73
|
+
# @param references [Array<Hash>] optional references to other schemas
|
|
74
|
+
# @return [Integer] the ID of the registered schema
|
|
75
|
+
def register(subject, schema, references: [], schema_type: 'PROTOBUF')
|
|
76
|
+
data = post("/subjects/#{@schema_context_prefix}#{CGI.escapeURIComponent(subject)}/versions",
|
|
77
|
+
body: { schemaType: schema_type,
|
|
78
|
+
references: references,
|
|
79
|
+
schema: schema.to_s }.to_json)
|
|
80
|
+
|
|
81
|
+
id = data.fetch('id')
|
|
82
|
+
|
|
83
|
+
@logger.info "Registered schema for subject `#{@schema_context_prefix}#{subject}`; id = #{id}"
|
|
84
|
+
|
|
85
|
+
id
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @param subject [String]
|
|
89
|
+
# @return [Array<Hash>] an array of versions for the given subject
|
|
90
|
+
def subject_versions(subject)
|
|
91
|
+
get("/subjects/#{@schema_context_prefix}#{CGI.escapeURIComponent(subject)}/versions", idempotent: true)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def get(path, **options)
|
|
97
|
+
request(path, method: :get, **options)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def put(path, **options)
|
|
101
|
+
request(path, method: :put, **options)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def post(path, **options)
|
|
105
|
+
request(path, method: :post, **options)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def request(path, **options)
|
|
109
|
+
options = { expects: 200 }.merge!(options)
|
|
110
|
+
path = File.join(@path_prefix, path) unless @path_prefix.nil?
|
|
111
|
+
response = @connection.request(path: path, **options)
|
|
112
|
+
JSON.parse(response.body)
|
|
113
|
+
rescue Excon::Error => e
|
|
114
|
+
@logger.error("Error while requesting #{path}: #{e.response.body}")
|
|
115
|
+
raise
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|